Paukan & NPRWizard
Привет, я пытаюсь впихнуть затенение на основе ребер в реальном времени, не перегружая буфер кадра. Есть какие-нибудь хитрости, чтобы контуры оставались чёткими, но с минимальными затратами?
Конечно, дружище! Забей на этот полный Собель на каждом кадре. Бери карту глубины или нормалей из первого прохода, запусти дешевый 3x3 kernel на GPU и пиши результат сразу в буфер трафарета. Так у тебя только один дополнительный вычислительный проход, и GPU сможет избежать тяжелого смешивания до финальной композиции. Дальше – пробей эти контуры двухцветным проходом, просто плоский цвет и резкий переход. Используй float низкой точности для карты краев, чтобы уменьшить пропускную способность памяти. Если ты в поисках ультра-дешевого решения, подумай о signed distance field для простых фигур – это даст тебе чёткие линии с минимальными усилиями. И напоследок, фиксируй толщину линии на целых пиксельных значениях – GPU прорисует эти линии аккуратно без затрат на анти-алиасинг. Могу углубиться в математику, лежащую в основе этого трюка с трафаретом, если нужно!
Звучит неплохо, но лучше перепроверь порядок очистки трафарета — если останется старая информация о глубине, то проход по краям пойдет не так. И размер ядра сделай нечетным; ядра четного размера сдвигают контур наполовину пикселя и нарушают это правило целочисленной ширины, о котором ты говорил. Если все в порядке, я могу набросать тебе точный код шейдера, который тебе нужен.
Отлично подметил насчёт stencil и kernel parity – такие мелочи могут испортить чёткий край, если не заметить. Буду рад глянуть на твой шейдерный набросок, особенно как ты stencil pass в финальное изображение вливаешь. Не стесняйся рассказывать, с чем столкнулся, помогу разобраться и подправить в духе старой школы!
Вот набросок, без изысков. Первый проход записывает карту краев в трафарет.
```glsl
// Vertex – одинаковый для обоих проходов
layout(location = 0) in vec3 aPos;
layout(location = 1) in vec3 aNormal;
uniform mat4 uMVP;
out vec3 vNormal;
void main() {
vNormal = aNormal;
gl_Position = uMVP * vec4(aPos,1.0);
}
```
**Проход трафарета (вычисление)**
```glsl
layout(local_size_x = 16, local_size_y = 16) in;
layout(r8) uniform readonly image2D depthTex;
layout(r8) uniform writeonly image2D stencilTex;
void main() {
ivec2 id = ivec2(gl_GlobalInvocationID.xy);
float d = imageLoad(depthTex, id).r;
// дешевый 3‑пиксельный kernel – просто разница с соседями
float dL = imageLoad(depthTex, id + ivec2(-1,0)).r;
float dR = imageLoad(depthTex, id + ivec2(1,0)).r;
float dU = imageLoad(depthTex, id + ivec2(0,-1)).r;
float dD = imageLoad(depthTex, id + ivec2(0,1)).r;
float edge = max(abs(d-dL), abs(d-dR), abs(d-dU), abs(d-dD));
// порог для бинарного края
float outVal = edge > 0.02 ? 1.0 : 0.0;
imageStore(stencilTex, id, vec4(outVal));
}
```
**Финальный композитный (фрагмент)**
```glsl
layout(location = 0) out vec4 fragColor;
uniform sampler2D baseTex;
uniform sampler2D stencilTex;
uniform vec3 outlineColor = vec3(0.0,0.0,0.0);
in vec2 texCoord;
void main() {
vec3 base = texture(baseTex, texCoord).rgb;
float edge = texture(stencilTex, texCoord).r;
vec3 color = mix(base, outlineColor, edge);
fragColor = vec4(color,1.0);
}
```
Единственный нюанс – порог `edge > 0.02` – подкрути его, чтобы контролировать толщину линии. Если линия получается слишком тонкой, уменьши порог или используй 5-пиксельный kernel. Если слишком толстой – увеличь. Это должно обеспечить чистую линию, выровненную по целому пикселю, без дополнительного AA. Дай знать, если проход вычислений зависает; я посмотрю на компоновку памяти или упаковку.
Отличный скелет – держишь его стройным, но при этом крепким для быстрых операций в реальном времени. Несколько небольших правок: используй `int` вместо float для записи в трафарет; так каждый пиксель поместится в один бит, если использовать 8-битную глубину, или даже используй упакованный `uint` для скорости. И еще, ограничь диапазон значений глубины, чтобы избежать выхода за границы на краях – можно обернуть или использовать `clampToEdge`. Если столкнешься с зависаниями, попробуй запускать с использованием числа локальных потоков, кратного четному, и читай из текстуры глубины группами; так GPU не будет охлаждать свои кэши. И если когда-нибудь захочешь более толстую линию без сильного повышения порога, добавь обводку ядра до 5x5, но при этом пиши только центральный пиксель – это сохранит трафарет разреженным. Удачи с шейдингом!
Спасибо за подробности. Переходим на 8-битный трафарет с плотной упаковкой битов – это снизит полосу пропускания, но помни о шаблонах чтения/записи GPU – избегай разбросанных записей. Для блока 5x5, оставляй центральную запись и, если нужна корректировка ширины на ходу, кидай остальное в небольшой отдельный буфер. И еще, если с текстурой глубины проблемы с уровнем детализации, привяжи её с отключенной линейной фильтрацией для трафаретного прохода. Это поможет держать зависания под контролем. Дай знать, если столкнешься с какими-нибудь странностями синхронизации.
Отлично! Мне нравится, как ты держишь трафарет в натяжении и рисуешь локально – именно то, что хотел бы увидеть старый добрый растеризатор. Если LOD глубины все еще доставляет проблемы, попробуй отказаться от ближайшего соседа, или, лучше, заблокируй текстуру глубины на точечное сглаживание и дай трафарету пройти, как фильтр контурного рисунка, созданный штрихами. Помни, ты всегда можешь рассматривать пад 5x5 как мини-буфер "давления пера" – так ты сохранишь чистую линию и низкую пропускную способность. Удачи в поиске четких краев – просто не позволяй кэшу GPU решать за тебя!
Понял, зафиксирую фильтр на точку и сделаю буфер чувствительности к нажатию максимально стабильным. Если кэш начнёт вести себя как сумасшедший бармен, добавлю небольшой счётчик для принудительной очистки. Спасибо, что предупредил.