Покрытия больших поверхностей — асфальт, тротуарная плитка, бордюр, земля, двор, площадь — процедурой во фрагментном шейдере от мировых координат, без текстур и без тайла. Поперечная параметризация полосы, без которой не сделать ни колеи, ни разметку, ни грязь у бордюра; перевязка рядов; хребтовый шум для сетки трещин; глушение мелкого зерна с расстоянием против муара; цена всего этого в кадре. Используй, когда дорога или тротуар «полосатые», когда видно повтор текстуры на большой площади, когда поверхность в сотни метров надо покрыть без библиотеки текстур, или когда плоскость выглядит крашеным картоном.
Resources
2Install
npx skillscat add sashkosoft/godot-skilled/procedural-ground-surfaces Install via the SkillsCat registry.
Покрытия без тайла: дорога, тротуар, земля
Собран на улице вокруг панельного дома в изометрической игре: полосы длиной
300 м, камера и вплотную, и с 80 м (см. ORIGIN.md). Рабочий шейдер целиком —references/surface-shader.md.
Главное правило
Любая тайловая текстура на поверхности в сотни метров рано или поздно
начинает дышать сеткой. Не помогает ни размер тайла, ни стохастическая
выборка: период всё равно находится глазом, особенно на скользящей камере.
Считай покрытие от мировой точки в фрагментном шейдере. Тогда повтора нет
ни на каком масштабе, потому что рисунок уникален в каждой точке улицы.
Отдельная ловушка на входе: не клади на горизонталь текстуру, сделанную для
стены. Фасадная панель со швами, положенная плашмя, даёт ровные полосы через
весь кадр — и это читается как баг освещения, а не как неверный материал.
Поперечная ось — то, без чего покрытие остаётся серым полем
Одного шума мало. Дорога отличается от бетонной площадки тем, что у неё есть
поперечная структура: колеи там, где ездят, разметка по оси, намытый песок
у бордюра, крокодилья сетка по краям, где нет нагрузки.
Дай шейдеру два уникформа:
uniform float lat_center; // мировая координата середины полосы
uniform float lat_half; // её полуширина
...
float lat = clamp((w.y - lat_center) / max(lat_half, 0.001), -1.0, 1.0);
float edge = abs(lat); // 0 по оси, 1 у кромкиДальше всё строится на lat:
float track = exp(-pow((edge - 0.46) / 0.15, 2.0)); // две накатанные полосы
float silt = smoothstep(0.72, 1.0, edge); // песок у бордюра
float line = 1.0 - smoothstep(0.045, 0.075, abs(lat * lat_half));Важно: у каждой полосы своя ось, значит своя копия материала. Один
общий материал на все полосы означает один lat_center — и колеи посчитаются
от чужой середины. Копий получается по одной на полосу, это единицы.
Приёмы, которые решают
Перевязка. Прямая сетка «клетка в клетку» сразу читается фальшивкой, так
плитку не кладут. Сдвигай каждый второй ряд на полплиты:
float row = floor(w.y / plate);
vec2 cell = vec2(w.x / plate + mod(row, 2.0) * 0.5, w.y / plate);Хеш на ячейку — не только на тон. От h21(id) должны зависеть тон плиты,
ширина шва, просадка и наличие трещины. Одинаковый шов по всей площади —
второй по заметности признак процедурности после прямой сетки.
Хребтовый шум для трещин. Обычный fbm с порогом по |n − 0.5| рисует ровно
одну мажущую полосу на октаву, и асфальт выглядит заляпанным, а не
потрескавшимся. Ветвящуюся сетку даёт хребет:
float n = 1.0 - abs(vnoise(p) * 2.0 - 1.0);
s += a * n * n;Трёх октав хватает и на крупные разломы, и на мелкое ветвление — второй вызов
на другом масштабе стоит вдвое и на глаз не добавляет ничего.
Почти стёртая разметка. Она делает полосу дорогой за один приём, а её
состояние — заброшенной дорогой. Пунктир от fract(w.x / шаг), поверх —
износ крупным шумом.
Две вещи, которые сломают картинку
Муар на дальнем плане. Мелкое зерно на расстоянии превращается в
регулярный узор — ту же сетку, от которой уходили. Глуши высокие частоты по
глубине:
float fine = clamp(1.0 - length(VERTEX) / 55.0, 0.0, 1.0);
col *= 1.0 - (grain - 0.5) * 0.20 * fine;Боковые грани. Полоса — это бокс, и у него есть торцы. Им не нужны ни
колеи, ни разметка. В Godot return во фрагментной функции запрещён
(SHADER ERROR: Using 'return' in the 'fragment' processor function is incorrect), поэтому флаг и подмена результата в конце:
bool is_side = NORMAL.y < 0.5;
...
if (is_side) { col = col_b * (0.80 + se * 0.30); rough = 1.0; }Цена
Это полноэкранные плоскости: на каждый пиксель приходятся десятки выборок
шума. Два правила окупаются сразу:
- хеш без
sin.fract(sin(dot(p, ...)) * 43758.5453)— самое дорогое,
что тут может быть. Бериfract-хеш на трёх множителях; - переиспользуй поля шума. Посчитай два-три (крупные разводы, средний,
зерно) и строй из них всё остальное — грязь, износ разметки, намыв.
Каждый лишнийfbm— это четыре выборки на октаву на каждом пикселе.
Щебень, дроблёное зерно и подобное — одна выборка vnoise, не fbm: на
таком масштабе октавы неразличимы.
Мерить цену только секундомером. На этом проекте число FPS из
скриншот-харнесса на одинаковой работе давало 83, 4, 15 и 1 — и целый заход
оптимизации был сделан по ложной тревоге. Честный замер: фиксированное число
кадров, обычный запуск, часы снаружи. Подробно — скил measurement-sanity.
Результат на реальной сцене: с улицей 82 FPS, без улицы 73 — покрытие не
стоило ничего измеримого.
Трава и мелкая растительность
Дёшево и красиво для изометрической камеры — двумя слоями по одной карте
«где растёт»: цвет травы в шейдере земли издали и настоящие травинки
(без текстур и альфы) в участке вокруг камеры. Там же подорожник, клевер
и цветы — без новых отрисовок. Подробно, с ловушками —references/grass.md.
Рабочие привычки
- На каждую спорную поверхность — флаг, который её снимает (
--no-street).
Два кадра рядом отвечают за минуту. - Заведи процедурной поверхности
kind: плита, асфальт, земля — один шейдер,
ветка на вид. Материалов от этого меньше, а править в одном месте. - Земля без геометрии остаётся крашеным картоном, сколько шума ни клади.
Это честный предел приёма: под неё нужен рельеф, а не пиксели.
Пол квартала из слоёв
Слои по правилам от данных квартала + тайловые наборы, смешанные по высоте; формат наборов для поставщика — references/floor-layers.md.