Как стать автором
Обновить

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

Время на прочтение13 мин
Количество просмотров13K
Пример сгенерированного ландшафта
Пример сгенерированного ландшафта

Вступление

Привет!

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

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

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

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

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

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

Основа

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

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

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

shader_type canvas_item;

void fragment() {
	
}

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

shader_type canvas_item;

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

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

shader_type canvas_item;

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

Но вернёмся к нашей теме - генерации ландшафтов. Начнём генерацию карты высот. карта высот использует только один канал 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);
}

Шум

Теперь нужно как-то разнообразить наш ландшафт. Для этого можно комбинировать несколько слоев шума. Существует множество реализаций разных шумов, я буду использовать простой 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);
}

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

Рендер в HTerrain

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

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

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

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.

На него можем прикрепить простой скрипт для передвижения, взятый отсюда: 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 зададим пути в инспекторе к остальным узлам.

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

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

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

Terrain не видно полностью даже с Far = 5000
Terrain не видно полностью даже с 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 (к примеру).

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

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

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

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

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

...
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);
}

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

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;

...

Получим нормаль прямо из шума и выведем её при 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.

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.

texture_type = 3

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

Вернёмся к части 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 в редакторе.

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

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

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

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

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

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

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

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

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

Результат:

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

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

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

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

Теги:
Хабы:
+11
Комментарии1

Публикации

Истории

Работа

Ближайшие события