Paukan & NPRWizard
Paukan Paukan
Привет, я пытаюсь впихнуть затенение на основе ребер в реальном времени, не перегружая буфер кадра. Есть какие-нибудь хитрости, чтобы контуры оставались чёткими, но с минимальными затратами?
NPRWizard NPRWizard
Конечно, дружище! Забей на этот полный Собель на каждом кадре. Бери карту глубины или нормалей из первого прохода, запусти дешевый 3x3 kernel на GPU и пиши результат сразу в буфер трафарета. Так у тебя только один дополнительный вычислительный проход, и GPU сможет избежать тяжелого смешивания до финальной композиции. Дальше – пробей эти контуры двухцветным проходом, просто плоский цвет и резкий переход. Используй float низкой точности для карты краев, чтобы уменьшить пропускную способность памяти. Если ты в поисках ультра-дешевого решения, подумай о signed distance field для простых фигур – это даст тебе чёткие линии с минимальными усилиями. И напоследок, фиксируй толщину линии на целых пиксельных значениях – GPU прорисует эти линии аккуратно без затрат на анти-алиасинг. Могу углубиться в математику, лежащую в основе этого трюка с трафаретом, если нужно!
Paukan Paukan
Звучит неплохо, но лучше перепроверь порядок очистки трафарета — если останется старая информация о глубине, то проход по краям пойдет не так. И размер ядра сделай нечетным; ядра четного размера сдвигают контур наполовину пикселя и нарушают это правило целочисленной ширины, о котором ты говорил. Если все в порядке, я могу набросать тебе точный код шейдера, который тебе нужен.
NPRWizard NPRWizard
Отлично подметил насчёт stencil и kernel parity – такие мелочи могут испортить чёткий край, если не заметить. Буду рад глянуть на твой шейдерный набросок, особенно как ты stencil pass в финальное изображение вливаешь. Не стесняйся рассказывать, с чем столкнулся, помогу разобраться и подправить в духе старой школы!
Paukan Paukan
Вот набросок, без изысков. Первый проход записывает карту краев в трафарет. ```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. Дай знать, если проход вычислений зависает; я посмотрю на компоновку памяти или упаковку.
NPRWizard NPRWizard
Отличный скелет – держишь его стройным, но при этом крепким для быстрых операций в реальном времени. Несколько небольших правок: используй `int` вместо float для записи в трафарет; так каждый пиксель поместится в один бит, если использовать 8-битную глубину, или даже используй упакованный `uint` для скорости. И еще, ограничь диапазон значений глубины, чтобы избежать выхода за границы на краях – можно обернуть или использовать `clampToEdge`. Если столкнешься с зависаниями, попробуй запускать с использованием числа локальных потоков, кратного четному, и читай из текстуры глубины группами; так GPU не будет охлаждать свои кэши. И если когда-нибудь захочешь более толстую линию без сильного повышения порога, добавь обводку ядра до 5x5, но при этом пиши только центральный пиксель – это сохранит трафарет разреженным. Удачи с шейдингом!
Paukan Paukan
Спасибо за подробности. Переходим на 8-битный трафарет с плотной упаковкой битов – это снизит полосу пропускания, но помни о шаблонах чтения/записи GPU – избегай разбросанных записей. Для блока 5x5, оставляй центральную запись и, если нужна корректировка ширины на ходу, кидай остальное в небольшой отдельный буфер. И еще, если с текстурой глубины проблемы с уровнем детализации, привяжи её с отключенной линейной фильтрацией для трафаретного прохода. Это поможет держать зависания под контролем. Дай знать, если столкнешься с какими-нибудь странностями синхронизации.
NPRWizard NPRWizard
Отлично! Мне нравится, как ты держишь трафарет в натяжении и рисуешь локально – именно то, что хотел бы увидеть старый добрый растеризатор. Если LOD глубины все еще доставляет проблемы, попробуй отказаться от ближайшего соседа, или, лучше, заблокируй текстуру глубины на точечное сглаживание и дай трафарету пройти, как фильтр контурного рисунка, созданный штрихами. Помни, ты всегда можешь рассматривать пад 5x5 как мини-буфер "давления пера" – так ты сохранишь чистую линию и низкую пропускную способность. Удачи в поиске четких краев – просто не позволяй кэшу GPU решать за тебя!
Paukan Paukan
Понял, зафиксирую фильтр на точку и сделаю буфер чувствительности к нажатию максимально стабильным. Если кэш начнёт вести себя как сумасшедший бармен, добавлю небольшой счётчик для принудительной очистки. Спасибо, что предупредил.