Процедурная генерация 3D миров в Godot Engine при помощи GPU. Часть 1

Пример сгенерированного ландшафтаПример сгенерированного ландшафта

Вступление

Привет!

Недавно начал экспериментировать с процедурной генерацией и получил некоторые наработки, с которыми и хотелось бы поделится. Примеры я буду показывать на движке Godot, однако при надобности код можно перенести на любой другой современный движок.

Начало работы

Создадим новый проект и скачаем плагин Heightmap Terrain из AssetLib — он уже содержит всё необходимое для базовой работы с ландшафтами. Не забудем также включить плагин в настройках проекта.

Если всё сделано правильно, то в списке узлов появится HTerrain. Данный узел как можно догадаться, позволяет создавать ландшафты.

А как вообще система представляет для себя ландшафт? Он хранится в виде текстур: карты высот (Height Map), карты нормалей (Normal Map), карты смешивания текстур поверхностей (Splat Map), а также эти самые текстуры поверхностей.

Значит, чтобы сгенерировать ландшафт, нам нужно просто сгенерировать карту высот и в зависимости от неё все остальные текстуры? Мы можем выполнять этот процесс в обычном коде, но для больших Terrain, разрешением, допустим, больше 512×512, производительности процессора будет уже не хватать. И тут нам на помощь приходят видеокарта. Для этой задачи хорошо подходят обычные шейдеры.

Основа

Так как нам нужна 2D картинка мы будем использовать CanvasItem шейдеры. Создадим какой-нибудь узел, наследующийся от CanvasItem. Здесь подойдёт ColorRect, так как нам нужен просто какой-нибудь прямоугольник. Зададим ему размер 4097×4097 пикселей, так как это максимальный размер, поддерживаемый плагином.

image-loader.svg

В свойство Material добавим новый ShaderMaterial.

image-loader.svg

Наш прямоугольник должен стать прозрачным. Добавим базовый код:

shader_type canvas_item;

void fragment() {
	
}

В нашем случае функция fragment () вызывается для каждой точки изображения. Например, такой код зальёт всё изображение жёлтым.

shader_type canvas_item;

void fragment() {
	COLOR.rgb = vec3(1.0, 1.0, 0.0);
}

image-loader.svg

Переменная UV содержит позицию точки, причём в диапазонах от нуля до единицы. Как можно заметить, все составляющие векторных переменных можно перемешивать, получая комбинации в любом порядке. Здесь можно было также использовать UV.x, UV.yx, UV.yy и так далее. Это всё давало бы разные интересные результаты.

shader_type canvas_item;

void fragment() {
	COLOR.rg = UV.xy;
}

image-loader.svg

Но вернёмся к нашей теме — генерации ландшафтов. Начнём генерацию карты высот. карта высот использует только один канал R. Для удобства мы будем использовать значения от нуля до единицы.

Для начала сделаем заготовку острова — чем дальше точка от центра изображения — тем значение меньше.

shader_type canvas_item;

void fragment() {
	float dist = distance(UV, vec2(0.5, 0.5));
	float height = 1.0 - dist / 0.5;
	
	COLOR.rgb = vec3(height, 0.0, 0.0);
}

image-loader.svg

Шум

Теперь нужно как-то разнообразить наш ландшафт. Для этого можно комбинировать несколько слоев шума. Существует множество реализаций разных шумов, я буду использовать простой Simplex Noise отсюда:

https://github.com/curly-brace/Godot-3.0-Noise-Shaders

Добавим код шума в начало шейдера до функции fragment ()

shader_type canvas_item;

uniform vec2 offset;

vec3 mod289_3(vec3 x) {
    return x - floor(x * (1.0 / 289.0)) * 289.0;
}

vec2 mod289_2(vec2 x) {
    return x - floor(x * (1.0 / 289.0)) * 289.0;
}

vec3 permute(vec3 x) {
    return mod289_3(((x*34.0)+1.0)*x);
}

float snoise(vec2 v) {
    vec4 C = vec4(0.211324865405187,  // (3.0-sqrt(3.0))/6.0
                  0.366025403784439,  // 0.5*(sqrt(3.0)-1.0)
                 -0.577350269189626,  // -1.0 + 2.0 * C.x
                  0.024390243902439); // 1.0 / 41.0
    // First corner
    vec2 i  = floor(v + dot(v, C.yy) );
    vec2 x0 = v -   i + dot(i, C.xx);
    
    // Other corners
    vec2 i1;
    //i1.x = step( x0.y, x0.x ); // x0.x > x0.y ? 1.0 : 0.0
    //i1.y = 1.0 - i1.x;
    i1 = (x0.x > x0.y) ? vec2(1.0, 0.0) : vec2(0.0, 1.0);
    // x0 = x0 - 0.0 + 0.0 * C.xx ;
    // x1 = x0 - i1 + 1.0 * C.xx ;
    // x2 = x0 - 1.0 + 2.0 * C.xx ;
    vec4 x12 = vec4(x0.xy, x0.xy) + C.xxzz;
    x12.xy -= i1;
    
    // Permutations
    i = mod289_2(i); // Avoid truncation effects in permutation
    vec3 p = permute( permute( i.y + vec3(0.0, i1.y, 1.0 ))
    	+ i.x + vec3(0.0, i1.x, 1.0 ));
    
    vec3 m = max(0.5 - vec3(dot(x0,x0), dot(x12.xy,x12.xy), dot(x12.zw,x12.zw)), vec3(0.0));
    m = m*m ;
    m = m*m ;
    
    // Gradients: 41 points uniformly over a line, mapped onto a diamond.
    // The ring size 17*17 = 289 is close to a multiple of 41 (41*7 = 287)
    
    vec3 x = 2.0 * fract(p * C.www) - 1.0;
    vec3 h = abs(x) - 0.5;
    vec3 ox = floor(x + 0.5);
    vec3 a0 = x - ox;
    
    // Normalise gradients implicitly by scaling m
    // Approximation of: m *= inversesqrt( a0*a0 + h*h );
    m *= 1.79284291400159 - 0.85373472095314 * ( a0*a0 + h*h );
    
    // Compute final noise value at P
    vec3 g;
    g.x  = a0.x  * x0.x  + h.x  * x0.y;
    g.yz = a0.yz * x12.xz + h.yz * x12.yw;
    return 130.0 * dot(m, g);
}

void fragment() {
	float dist = distance(UV, vec2(0.5, 0.5));
	float height = 1.0 - dist / 0.5;
	
	COLOR.rgb = vec3(height, 0.0, 0.0);
}

Теперь мы можем использовать функцию snoise (). Также из редактора доступно свойство offset. Создадим вспомогательную функцию get_height (), которая будет возвращать высоту в точке.

...

float get_height(float x, float y) {
	float base_noise = snoise((vec2(x, y) + offset) * 2.0) * 0.5 + 0.5;
	
	float dist = distance(vec2(x, y), vec2(0.5, 0.5));
	float height = 1.0 - dist / 0.5;
	
	return height + base_noise / 4.0;
}

void fragment() {
	COLOR.rgb = vec3(get_height(UV.x, UV.y), 0.0, 0.0);
}

image-loader.svg

Меняя свойство offset мы можем получить другой результат.

image-loader.svgimage-loader.svg

Рендер в HTerrain

Для начала работы, такой картинки будет достаточно. Теперь нужно перенести изображение из нашего шейдера в сам ландшафт. Для этого поместим наш TextureRect в отдельный Viewport. При этом вне Viewport можно оставить копию для отладки.

image-loader.svg

Зададим размер viewport равный размеру TextureRect. Также необходимо включить поддержку HDR, которая позволяет принимать значения за пределами нуля и единицы — это нужно для карты высот. Update Mode ставим на Disabled, т.к. мы будем обновлять Viewport из кода только один раз чтобы получить нашу картинку. Usage установим на 3D No-Effect, т.к. 3D необходимо для HDR.

image-loader.svg

Теперь создадим некий объект, который будет отвечать за генерацию. Прикрепим к нему скрипт.

image-loader.svg

extends Node

# Импортируем необходимые классы из плагина
const HTerrain = preload("res://addons/zylann.hterrain/hterrain.gd")
const HTerrainData = preload("res://addons/zylann.hterrain/hterrain_data.gd")
const HTerrainTextureSet = preload("res://addons/zylann.hterrain/hterrain_texture_set.gd")

# Нам необходим объект Viewport с нашим TextureRect
export (NodePath) var viewport_path :NodePath
onready var viewport :Viewport = get_node(viewport_path)

export (NodePath) var shader_node_path :NodePath
onready var shader_node :CanvasItem = get_node(shader_node_path)

func _ready():
	randomize()
	
	# Зададим случайное смещение параметра offse шейдера, для того чтобы при каждом запуске получать разные результаты
	shader_node.material.set_shader_param("offset", Vector2(rand_range(-100.0, 100.0), rand_range(-100, 100)))
	
	# Создадим объект с данными Terrain
	var terrain_data = HTerrainData.new()
	terrain_data.resize(4097)
	
	# Получим изображение, которое нужно заменить нашим Heightmap
	var heightmap :Image = terrain_data.get_image(HTerrainData.CHANNEL_HEIGHT)
	
	# Заставим Viewport обновиться
	viewport.render_target_update_mode = Viewport.UPDATE_ONCE
	
	# Благодаря этой конструкции мы можем пропустить выполнение пары кадров для этого кода, чтобы Viewport успел зарендерить картинку.
	yield(get_tree(), "idle_frame")
	yield(get_tree(), "idle_frame")
	
	# Получим картинку с Viewport
	var computed_heightmap :Image = viewport.get_texture().get_data()
	
	# Заменим пустую картинку в нашем Terrain Data полученным Heightmap
	heightmap.copy_from(computed_heightmap)
	
	# Создаём узел Terrain
	var terrain = HTerrain.new()
	terrain.set_shader_type(HTerrain.SHADER_CLASSIC4_LITE)
	terrain.set_data(terrain_data)
	terrain.translation = Vector3(-2048.5, -25, -2048.5)
	
	# Добавим узел Terrain на сцену
	add_child(terrain)

Добавим на сцену узел Camera.

image-loader.svg

На него можем прикрепить простой скрипт для передвижения, взятый отсюда: https://github.com/adamviola/simple-free-look-camera

class_name FreelookCamera extends Camera

export(float, 0.0, 1.0) var sensitivity = 0.25

# Mouse state
var _mouse_position = Vector2(0.0, 0.0)
var _total_pitch = 0.0

# Movement state
var _direction = Vector3(0.0, 0.0, 0.0)
var _velocity = Vector3(0.0, 0.0, 0.0)
var _acceleration = 30
var _deceleration = -10
var _vel_multiplier = 4

# Keyboard state
var _w = false
var _s = false
var _a = false
var _d = false
var _q = false
var _e = false

func _input(event):
	# Receives mouse motion
	if event is InputEventMouseMotion:
		_mouse_position = event.relative
	
	# Receives mouse button input
	if event is InputEventMouseButton:
		match event.button_index:
			BUTTON_RIGHT: # Only allows rotation if right click down
				Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED if event.pressed else Input.MOUSE_MODE_VISIBLE)
			BUTTON_WHEEL_UP: # Increases max velocity
				_vel_multiplier = clamp(_vel_multiplier * 1.1, 0.2, 20)
			BUTTON_WHEEL_DOWN: # Decereases max velocity
				_vel_multiplier = clamp(_vel_multiplier / 1.1, 0.2, 20)

	# Receives key input
	if event is InputEventKey:
		match event.scancode:
			KEY_W:
				_w = event.pressed
			KEY_S:
				_s = event.pressed
			KEY_A:
				_a = event.pressed
			KEY_D:
				_d = event.pressed
			KEY_Q:
				_q = event.pressed
			KEY_E:
				_e = event.pressed

# Updates mouselook and movement every frame
func _process(delta):
	_update_mouselook()
	_update_movement(delta)

# Updates camera movement
func _update_movement(delta):
	# Computes desired direction from key states
	_direction = Vector3(_d as float - _a as float, 
						 _e as float - _q as float,
						 _s as float - _w as float)
	
	# Computes the change in velocity due to desired direction and "drag"
	# The "drag" is a constant acceleration on the camera to bring it's velocity to 0
	var offset = _direction.normalized() * _acceleration * _vel_multiplier * delta \
		+ _velocity.normalized() * _deceleration * _vel_multiplier * delta
	
	# Checks if we should bother translating the camera
	if _direction == Vector3.ZERO and offset.length_squared() > _velocity.length_squared():
		# Sets the velocity to 0 to prevent jittering due to imperfect deceleration
		_velocity = Vector3.ZERO
	else:
		# Clamps speed to stay within maximum value (_vel_multiplier)
		_velocity.x = clamp(_velocity.x + offset.x, -_vel_multiplier, _vel_multiplier)
		_velocity.y = clamp(_velocity.y + offset.y, -_vel_multiplier, _vel_multiplier)
		_velocity.z = clamp(_velocity.z + offset.z, -_vel_multiplier, _vel_multiplier)
	
		translate(_velocity * delta)

# Updates mouse look 
func _update_mouselook():
	# Only rotates mouse if the mouse is captured
	if Input.get_mouse_mode() == Input.MOUSE_MODE_CAPTURED:
		_mouse_position *= sensitivity
		var yaw = _mouse_position.x
		var pitch = _mouse_position.y
		_mouse_position = Vector2(0, 0)
		
		# Prevents looking up/down too far
		pitch = clamp(pitch, -90 - _total_pitch, 90 - _total_pitch)
		_total_pitch += pitch
	
		rotate_y(deg2rad(-yaw))
		rotate_object_local(Vector3(1,0,0), deg2rad(-pitch))

Для Terrain Generator зададим пути в инспекторе к остальным узлам.

image-loader.svg

Не забудем скрыть отладочный ColorRect, иначе он будет мешать.

image-loader.svg

Рекомендую также увеличить свойство Far камеры, иначе мы не будем видеть весь Terrain.

image-loader.svg

При запуске на первый взгляд видна просто белая плоскость. Дело в том, что мы выводили значения только от нуля до единицы. Необходимо это исправить.

Terrain не видно полностью даже с Far = 5000Terrain не видно полностью даже с Far = 5000

Вернёмся к шейдеру и добавим переменную max_height.

shader_type canvas_item;

uniform vec2 offset = vec2(0, 0);
uniform float max_height = 1.0;

В fragment () добавим умножение на max_height.

void fragment() {
	COLOR.rgb = vec3(get_height(UV.x, UV.y) * max_height, 0.0, 0.0);
}

У ColorRect, находящегося в Viewport укажем max_height в редакторе на 300 (к примеру).

image-loader.svg

Добавим на сцену также Directional Light с Shadow/Enabled = True.

image-loader.svg

На данный момент результат выглядит как-то так.

image-loader.svg

Для лучшего понимания в редакторе добавим Plane MeshInstance, который будет показывать уровень воды.

image-loader.svg

Артефакты можно убрать увеличив Near камеры.

image-loader.svg

Вернёмся к шейдеру. Попробуем комбинировать несколько слоев шума чтобы получать лучший результат.

...
float get_height(float x, float y) {
	float base_noise = snoise((vec2(x, y) + offset) * 2.0) * 0.5 + 0.5;
	
	float dist = distance(vec2(x, y), vec2(0.5, 0.5));
	float inv_dist = 1.0 - dist / 0.5;
	
	float base = inv_dist / 0.25 * base_noise;
	
	float layer_noise = snoise((vec2(x, y) + offset) * 10.0) * 0.5;
	
	float result = base / 4.0 + layer_noise / 60.0;
	
	return result;
}

void fragment() {
	COLOR.rgb = vec3(get_height(UV.x, UV.y) * max_height, 0.0, 0.0);
}

image-loader.svg

Это уже больше похоже на остров.

image-loader.svg

Normal Map

Теперь нужно сгенерировать все остальные текстуры для terrain. Добавим переменную, которая будет показывать какую именно текстуру нужно сгенерировать.

0 — Height Map

1 — Normal Map

2 — Splat Map

shader_type canvas_item;

uniform vec2 offset = vec2(0, 0);
uniform float max_height = 1.0;
uniform int texture_type = 0;

...

Добавим в код функцию encode_normal ()

vec3 encode_normal(vec3 n){
	return (0.5 * (n + vec3(1.0))).rbg;
}

Получим нормаль прямо из шума и выведем её при texture_type == 1.

...

vec3 encode_normal(vec3 n){
	return (0.5 * (n + vec3(1.0))).rbg;
}

float get_height(float x, float y) {
	float base_noise = snoise((vec2(x, y) + offset) * 2.0) * 0.5 + 0.5;
	
	float dist = distance(vec2(x, y), vec2(0.5, 0.5));
	float inv_dist = 1.0 - dist / 0.5;
	
	float base = inv_dist / 0.25 * base_noise;
	
	float layer_noise = snoise((vec2(x, y) + offset) * 10.0) * 0.5;
	
	float result = base / 4.0 + layer_noise / 60.0;
	
	return result;
}

void fragment() {
	float height = get_height(UV.x, UV.y);
	float real_height = height * max_height;
	
	float h_right = max_height * get_height(UV.x + 0.0000244140625, UV.y);
	float h_forward = max_height * get_height(UV.x, UV.y + 0.0000244140625);
	vec3 normal = normalize(vec3(real_height - h_right, 0.1, h_forward - real_height));
	
	switch (texture_type){
		case 0:
			COLOR.rgb = vec3(real_height, 0.0, 0.0);
			break;
		case 1:
			COLOR.rgb = encode_normal(normal);
			break;
	}
}

Результат при texture_type == 1.

image-loader.svg

Splat Map

При генерации Splat Map появляется первая серьёзная проблема. Каждый пиксель Splat Map представляет вес четырёх текстур в виде значений четырёх каналов R, G, B, A. Но мы не можем правильно передать текстуру с прозрачностью из шейдера, так как при считывании значение A приближается к значение R, G, B и изначальное значение теряется. Поэтому я отдельно рендерю карту с R, G, B, а затем карту со значениями A. Затем они объединяются со стороны GDScript. Это очень сильно снижает производительность, но мне пока что не приходит на ум лучшего решения без изменения кода плагина.

2 = Splat Map

3 = Splat Map A

Можно высчитывать значения Splat Map в зависимости от высоты и нормали. Triplanar Mapping поддерживается только для четвертого канала, поэтому для склонов мы будем использовать его. Хотя в моем случае склонов обычно не генерируется.

R = Земля

G = Песок

B =

A = Склоны

vec3 encode_normal(vec3 n){
	return (0.5 * (n + vec3(1.0))).rbg;
}

vec4 linear_interpolate(vec4 a, vec4 b, float ammount){
	return a + (b - a) * ammount;
}

float get_height(float x, float y) {
	float base_noise = snoise((vec2(x, y) + offset) * 2.0) * 0.5 + 0.5;
	
	float dist = distance(vec2(x, y), vec2(0.5, 0.5));
	float inv_dist = 1.0 - dist / 0.5;
	
	float base = inv_dist / 0.25 * base_noise;
	
	float layer_noise = snoise((vec2(x, y) + offset) * 10.0) * 0.5;
	
	float result = base / 4.0 + layer_noise / 60.0;
	
	return result;
}

void fragment() {
	float height = get_height(UV.x, UV.y);
	float real_height = height * max_height;
	
	float h_right = max_height * get_height(UV.x + 0.0000244140625, UV.y);
	float h_forward = max_height * get_height(UV.x, UV.y + 0.0000244140625);
	vec3 normal = normalize(vec3(real_height - h_right, 0.1, h_forward - real_height));
	
	vec4 splat = vec4(1.0, 0.0, 0.0, 0.0);
	float slope = 4.0 * dot(normal, vec3(0.0, 1.0, 0.0)) - 2.0;
	float slope_amount = clamp(1.0 - slope, 0.0, 1.0);
	float sand_amount = clamp(30.0 - real_height, 0.0, 1.0);
	
	splat = linear_interpolate(splat, vec4(0.0,1.0,0.0,0.0), sand_amount);
	splat = linear_interpolate(splat, vec4(0.0,0.0,0.0,1.0), slope_amount);
	
	switch (texture_type){
		case 0:
			COLOR.rgb = vec3(real_height, 0.0, 0.0);
			break;
		case 1:
			COLOR.rgb = encode_normal(normal);
			break;
		case 2:
			COLOR.rgb = splat.rgb;
			break;
		case 3:
			COLOR.rgb = vec3(splat.a, 0, 0);
			break;
		
	}
}

texture_type = 2.

image-loader.svg

texture_type = 3

image-loader.svg

Если сделать значения высоты сильно резче:

image-loader.svg

Вернёмся к части GDScript.

extends Node

# Импортируем необходимые классы из плагина
const HTerrain = preload("res://addons/zylann.hterrain/hterrain.gd")
const HTerrainData = preload("res://addons/zylann.hterrain/hterrain_data.gd")
const HTerrainTextureSet = preload("res://addons/zylann.hterrain/hterrain_texture_set.gd")

# Набор текстур Terrain созданный в редакторе
const texture_set = preload("res://terrain_texture_set.tres")

# Нам необходим объект Viewport с нашим TextureRect
export (NodePath) var viewport_path :NodePath
onready var viewport :Viewport = get_node(viewport_path)

export (NodePath) var shader_node_path :NodePath
onready var shader_node :CanvasItem = get_node(shader_node_path)

func _ready():
	randomize()
	
	# Зададим случайное смещение параметра offse шейдера, для того чтобы при каждом запуске получать разные результаты
	shader_node.material.set_shader_param("offset", Vector2(rand_range(-100.0, 100.0), rand_range(-100, 100)))
	
	# Создадим объект с данными Terrain
	var terrain_data = HTerrainData.new()
	terrain_data.resize(4097)
	
	# Получим изображения
	var heightmap :Image = terrain_data.get_image(HTerrainData.CHANNEL_HEIGHT)
	var normalmap :Image = terrain_data.get_image(HTerrainData.CHANNEL_NORMAL)
	var splatmap :Image = terrain_data.get_image(HTerrainData.CHANNEL_SPLAT)
	
	# --------
	# Укажем шейдеру какую текстуру мы хотим получить
	shader_node.material.set_shader_param("texture_type", 0)
	
	# Заставим Viewport обновиться
	viewport.render_target_update_mode = Viewport.UPDATE_ONCE
	
	# Благодаря этой конструкции мы можем пропустить выполнение пары кадров для этого кода, чтобы Viewport успел зарендерить картинку.
	yield(get_tree(), "idle_frame")
	yield(get_tree(), "idle_frame")
	
	# Получим картинку с Viewport
	var computed_heightmap :Image = viewport.get_texture().get_data()
	
	# --------
	# Укажем шейдеру какую текстуру мы хотим получить
	shader_node.material.set_shader_param("texture_type", 1)
	
	# Заставим Viewport обновиться
	viewport.render_target_update_mode = Viewport.UPDATE_ONCE
	
	# Благодаря этой конструкции мы можем пропустить выполнение пары кадров для этого кода, чтобы Viewport успел зарендерить картинку.
	yield(get_tree(), "idle_frame")
	yield(get_tree(), "idle_frame")
	
	# Получим картинку с Viewport
	var computed_normalmap :Image = viewport.get_texture().get_data()
	# --------
	# Укажем шейдеру какую текстуру мы хотим получить
	shader_node.material.set_shader_param("texture_type", 2)
	
	# Заставим Viewport обновиться
	viewport.render_target_update_mode = Viewport.UPDATE_ONCE
	
	# Благодаря этой конструкции мы можем пропустить выполнение пары кадров для этого кода, чтобы Viewport успел зарендерить картинку.
	yield(get_tree(), "idle_frame")
	yield(get_tree(), "idle_frame")
	
	# Получим картинку с Viewport
	var computed_splatmap :Image = viewport.get_texture().get_data()
	# --------
	# Укажем шейдеру какую текстуру мы хотим получить
	shader_node.material.set_shader_param("texture_type", 3)
	
	# Заставим Viewport обновиться
	viewport.render_target_update_mode = Viewport.UPDATE_ONCE
	
	# Благодаря этой конструкции мы можем пропустить выполнение пары кадров для этого кода, чтобы Viewport успел зарендерить картинку.
	yield(get_tree(), "idle_frame")
	yield(get_tree(), "idle_frame")
	
	# Получим картинку с Viewport
	var computed_splatmap_a :Image = viewport.get_texture().get_data()
	# --------
	
	# Объединим Splat Map RGB и Splat Map A
	computed_splatmap.lock()
	computed_splatmap_a.lock()
	for x in range(computed_splatmap.get_width()):
		for y in range(computed_splatmap.get_height()):
			var p :Color = computed_splatmap.get_pixel(x, y)
			p.a = computed_splatmap_a.get_pixel(x, y).r;
			computed_splatmap.set_pixel(x, y, p)
	
	computed_splatmap.unlock()
	computed_splatmap_a.unlock()
	
	# Вернём полученные текстуры в Terrain Data
	heightmap.copy_from(computed_heightmap)
	normalmap.copy_from(computed_normalmap)
	splatmap.copy_from(computed_splatmap)
	
	# Создаём узел Terrain
	var terrain = HTerrain.new()
	terrain.set_shader_type(HTerrain.SHADER_CLASSIC4_LITE)
	terrain.set_shader_param("u_triplanar", true)
	terrain.set_shader_param("u_tile_reduction", Quat(1.0, 1.0, 1.0, 1.0))
	terrain.set_shader_param("u_depth_blending", true)
	terrain.set_texture_set(texture_set)
	terrain.set_data(terrain_data)
	terrain.translation = Vector3(-2048.5, -25, -2048.5)
	
	# Добавим узел Terrain на сцену
	add_child(terrain)

Здесь было бы неплохо объединить четыре одинаковых фрагмента получения текстур в функцию, однако это не получается сделать из-за использования yield (), который завершает выполнение функции и возвращает значение указывающее какой сейчас оператор выполняется.

Также появилась переменная texture_set. Нужно создать TextureSet в редакторе.

Добавим в проект текстуры.

image-loader.svg

Создадим узел HTerrain в редакторе, чтобы получить доступ к его инструментам.

Нажмём на Import… в окне управления текстурами

image-loader.svg

Устанавливаем все четыре набора текстур и импортируем.

image-loader.svg

В инспекторе выберем ресурс из свойства Texture Set и сохраним его в файл «terrain_texture_set.tres»

image-loader.svgimage-loader.svg

Удаляем ненужный HTerrain и тестируем проект.

image-loader.svg

Если сделать вывод функции get_height () более резким, то появляются склоны.

// 2.0 -> 20.0
float base_noise = snoise((vec2(x, y) + offset) * 20.0) * 0.5 + 0.5;

image-loader.svg

Теперь можно создать более реалистичную воду. Я использовал этот шейдер: https://github.com/godot-extended-libraries/godot-realistic-water

Результат:

image-loader.svgimage-loader.svg

Понравилась статья?

Здесь вы можете поддержать меня, а также скачать готовый проект:

https://gtutorials.gumroad.com/l/generation_part_1

© Habrahabr.ru