Декали: разметка и детали поверхности
Спроецировать разметку, настроить rendering layers, изменить ORM без перекрашивания и устранить ошибки маски и свечения.
Когда нужна декаль
Декаль проецирует текстуру на существующие поверхности: разметку, следы, повреждения или локальное изменение материала. Она не добавляет геометрию, не вырезает отверстие и не меняет коллизию. Для рельефа силуэта нужен меш, для физической ямы — отдельная геометрия столкновений.
Готовый пример ниже проверен в Forward+/Vulkan текущей сборки 1.0.0. В сцене уже настроены материалы, свет, тени и сглаживание.
Запустить разметку пола
Скачать галерею с декалью. Распакуйте проект, откройте project.reckfell в Reckfell с C# SDK, соберите C# и запустите Main.rscene. Внешние текстуры и модели не нужны: DecalGallery3D.cs создаёт разметку, CinematicGallery3D.cs — галерею.
У корня Preview=Albedo показывает цветную разметку, OrmOnly — изменение шероховатости и металличности по той же маске. MarkingEnabled=false выключает декаль при запуске. Эти экспортированные значения читаются в _Ready; из работающего кода меняйте Marking.Visible или вызывайте ShowOrmOnly() после _Ready.
В примере пол получает Layers=2, декаль — CullMask=2. Это битовая маска слоёв рендеринга, а не физики: значение 2 выбирает второй слой. Для слоя n используйте 1u << (n - 1). Камера также должна видеть слой получателя.
DecalGallery3D.cs
using System.Collections.Generic;
using Reckfell;
public partial class DecalGallery3D : CinematicGallery3D
{
public enum SurfacePreview { Albedo, OrmOnly }
[Export] public SurfacePreview Preview { get; set; } = SurfacePreview.Albedo;
[Export] public bool MarkingEnabled { get; set; } = true;
public Decal Marking { get; private set; } = null!;
private readonly List<MeshInstance3D> floorTiles = new();
public override void _Ready()
{
base._Ready();
foreach (Entity child in GetChildren())
if (child is MeshInstance3D tile && tile.Mesh is BoxMesh box &&
Mathf.IsEqualApprox(tile.Position.Y, -0.055f) && Mathf.IsEqualApprox(box.Size.Y, 0.12f))
floorTiles.Add(tile);
SetFloorLayers(2);
Marking = new Decal
{
Position = new Vector3(0, 0.12f, -1.5f), Size = new Vector3(4.8f, 0.7f, 4.8f),
TextureAlbedo = CreateMarking(), CullMask = 2, AlbedoMix = 1,
Modulate = Colors.White, UpperFade = 0, LowerFade = 0,
NormalFade = 0.5f, DistanceFadeEnabled = false, Visible = MarkingEnabled
};
AddChild(Marking);
if (Preview == SurfacePreview.OrmOnly) ShowOrmOnly();
}
public void ShowOrmOnly()
{
using var image = Image.CreateEmpty(16, 16, false, Image.Format.Rgba8);
image.Fill(new Color(1, 0, 1)); // R=AO, G=roughness, B=metallic.
Marking.TextureOrm = ImageTexture.CreateFromImage(image);
Marking.AlbedoMix = 0; // Keep TextureAlbedo for its alpha mask.
}
public void SetFloorLayers(uint mask)
{
foreach (var tile in floorTiles) tile.Layers = mask;
}
// Alpha is the projection mask. Transparent pixels retain the floor material.
private static ImageTexture CreateMarking()
{
const int size = 1024;
using var image = Image.CreateEmpty(size, size, false, Image.Format.Rgba8);
for (int y = 0; y < size; y++) for (int x = 0; x < size; x++)
{
float u = (x + 0.5f) / size * 2 - 1, v = (y + 0.5f) / size * 2 - 1;
float a = Mathf.Abs(u), b = Mathf.Abs(v);
float edge = Mathf.Max(Mathf.Max(a, b), (a + b) * 0.70710678f);
float rim = 1 - Mathf.SmoothStep(0.006f, 0.01f, Mathf.Abs(edge - 0.85f));
float band = Mathf.SmoothStep(0.74f, 0.748f, edge) * (1 - Mathf.SmoothStep(0.80f, 0.808f, edge));
float stripe = Mathf.PosMod(u + v, 0.14f) < 0.075f ? 1 : 0.12f;
float bracket = a > 0.34f && a < 0.43f && b > 0.27f && b < 0.43f ? 1 : 0;
bracket = Mathf.Max(bracket, b > 0.34f && b < 0.43f && a > 0.27f && a < 0.43f ? 1 : 0);
float arrow = 0;
for (int i = 0; i < 3; i++)
if (Mathf.Abs(v + 0.15f + i * 0.18f - a * 0.7f) < 0.035f && a < 0.2f) arrow = 1;
float alpha = Mathf.Max(rim, Mathf.Max(band * stripe, Mathf.Max(bracket, arrow)));
Color color = band > 0.1f ? new Color(0.2f, 0.52f, 0.82f, alpha) : new Color(0.82f, 0.89f, 0.95f, alpha);
image.SetPixel(x, y, color);
}
image.GenerateMipmaps();
return ImageTexture.CreateFromImage(image);
}
}Разместить проекцию
Size=(4.8, 0.7, 4.8) — полный размер объёма по локальным осям. Текстура раскладывается по X/Z, глубина идёт по Y. При исходном повороте пример рисует на полу; для стены поверните саму Decal так, чтобы объём пересекал стену и направление проекции соответствовало её поверхности.
Position=(0, 0.12, -1.5) помещает пол внутрь объёма. Если перенести декаль вверх за пределы пересечения, рисунок исчезнет. Увеличивать яркость в такой ситуации бесполезно: сначала проверьте положение, ориентацию и Size. Слишком большая глубина может захватить лишнюю геометрию.
UpperFade и LowerFade смягчают вклад к противоположным границам локальной Y; в примере оба равны 0 для равномерной разметки. NormalFade ослабляет проекцию на неподходяще ориентированных поверхностях и не является силой normal map. Size ограничивается снизу 0.001 по каждой оси: нулевой размер не служит выключателем.
Менять материал без перекрашивания
| Канал | Что меняет | Маска |
|---|---|---|
| TextureAlbedo | базовый цвет | alpha этой текстуры и Modulate.A |
| TextureNormal | нормаль освещения | alpha TextureAlbedo и Modulate.A |
| TextureOrm | R: AO, G: roughness, B: metallic | alpha TextureAlbedo и Modulate.A |
| TextureEmission | добавочное свечение по RGB | собственный RGB, не alpha albedo |
Для ORM-only эффекта оставьте TextureAlbedo и установите AlbedoMix=0. Метод ShowOrmOnly() назначает ORM=(1, 0, 1): AO не ослаблен, шероховатость нулевая, металличность полная внутри маски. Это заметный учебный контраст; для финального материала подберите менее крайние значения.
Удалять TextureAlbedo вместо AlbedoMix=0 нельзя: в проверенной конфигурации вместе с ней пропадает маска для ORM. Normal тоже использует albedo-маску; его импорт и ориентация из сторонних программ этим примером не проверены.
Прозрачные участки TextureAlbedo сохраняют исходный материал. Для импортированных файлов сохраните alpha, проверьте фильтрацию и mipmaps; генератор примера создаёт mipmaps перед ImageTexture.CreateFromImage. Разновидности Texture2D не взаимозаменяемы для atlas декалей: не считайте ViewportTexture или AtlasTexture обычным статическим изображением.
Свечение и выключение
Emission — отдельный вклад. Нативная проверка подтвердила его видимость без TextureAlbedo и при Modulate.A=0. Для областей без свечения используйте чёрный RGB emission-текстуры; одной прозрачности недостаточно. EmissionEnergy=0 убирает свечение, но не остальные каналы.
Для полного отключения экземпляра установите Marking.Visible=false. Glow может добавить ореол яркому emission, но свечение декали само по себе не создаёт Light3D для соседних мешей.
Убрать дальние декали
DistanceFadeEnabled включает затухание по расстоянию от камеры до позиции Decal. До DistanceFadeBegin вклад сохраняется, на следующем DistanceFadeLength плавно уменьшается; за Begin+Length декаль исключается. Используйте неотрицательное начало и положительную длину перехода.
Проверка сравнивает Begin=0, Length=0.1, когда декаль далеко от камеры, и Begin=100, Length=1, когда она ещё до начала затухания. Это не рекомендуемые настройки любой игры. Distance fade не удаляет Entity, не останавливает её скрипты и не является стримингом мира.
Если результат неправильный
| Симптом | Проверка |
|---|---|
| Рисунок не появился | Visible, TextureAlbedo, пересечение объёма с поверхностью, CullMask и Layers, видимость получателя для камеры |
| Захвачены соседние объекты | Уменьшить Size.Y или разделить rendering layers |
| ORM исчез после удаления albedo | Вернуть albedo-маску и использовать AlbedoMix=0 |
| Alpha=0, а свечение осталось | Emission независим от Modulate.A; выключить Visible или EmissionEnergy |
| Декаль исчезает вдали | DistanceFadeEnabled, Begin и Length |
| При смене маски в Compatibility возникает ошибка шейдера | Этот проход не принят для готовой сцены: наблюдалась ShaderGLES3: Parameter "spec" is null. Для воспроизведения примера используйте Forward+ |
Снимки сделаны актуальным локальным runtime: 2560×1440, внутренний масштаб 1.5, MSAA 8× и TAA. Камера, свет и экспозиция одинаковы; меняется только показанное состояние декали. Нативные проверки подтверждают указанные операции с albedo, ORM, emission, масками и дистанцией. Это не подтверждение паритета renderer, прозрачных получателей, множества перекрывающихся декалей или производительности на других устройствах.