Визуализация огромной поверхности океана достаточно ресурсоемкая задача. Во-первых, из-за большого числа видимых пикселей и сложности фрагментного шейдера, используемого при отрисовке. Во-вторых, из-за выборок из текстуры в вершинном шейдере. Соответственно первая проблема остро встает при больших разрешениях, а вторая для более старых видеокарт.
Со второй проблемой бороться за счет реалищации грамотной системы распределения уровня деталлизации и использования как можно меньшего колличества вершин.
Одним из самых оптимальных способов уменьшения уровня детализации в зависимости от расстояния до наблюдателя является использование проекционной сетки.
В классическом варианте алгоритма экран заполняется равномерной сеткой полигонов. Затем производится перевод сетки из экранных координат в видовые и проецирование на плоскость воды. На самом деле, алгоритм, не смотря на кажущуюся простоту, таит в себе огромное количество проблем. Так, например, разработчики CryEngine 2 отказались от его использования.
Вместо проецирования сетки на плоскость воды, можно её просто "прикрепить" к камере. Таким образом, камера будет двигаться вместе с фрагментом сетки, причем более деталлизированная часть будет ближе к наблюдателю. Распределение деталлизации в таком случае будет менее эффективным, зато такой подход значительно проще в реализации.
В некоторых случаях сетка не проецируется на плоскость воды (рис. 16) или при её проецировании, полученная трапеция имеет слишком большую площадь. Второй случай плох, потому что сетка полигонов слишком растянется и будет иметь недостаточную плотность близи камеры. Будут видны артефакты.
С этим можно бороться, поворачивая камеру для проекции так, чтобы она смотрела под достаточно большим углом на плоскость воды, либо не растягивалать сетку больше установленной дистанции видимости.
С другой стороны, можно заменить проецирование определением пересечения пирамиды видимости камеры и плоскости воды. Уравнения для плоскостей пирамиды видимости можно вычислить из модельно видовой матрицы и матрицы проекции. Потом можно найти граничные векторы пирамиды и их точки пересечения с плоскостью.
Полученные точки можно передать в шейдер, выполняющий визуализацию водной поверхности. Вычислять интерполированную позицию точки в плоскости воды можно используя, например, барицентрические координаты.
Стоит заметить, что в некоторых случаях точки пересечения могут образовывать четырехугольник, а в некоторых пятиугольник. Нужно либо это учитывать при интерполяции вершин водной поверхности, либо приводить все к одной фигуре.
Несмотря на то, что при использовании перспективной проекции, вблизи камеры плотность точек водной поверхности будет значительно больше чем вдали, на практике, при небольшом размере сетки, этого недостаточно. Поэтому, чтобы не перегружать GPU огромным количеством полигонов, лучше распределять плотность точек неравномерно, а располагать большую часть точек у той стороны ограничивающего сетку прямоугольника, которая будет ближе к камере.
В предложенной реализации была использована следующая формула для распределения точек в проекционной сетке:
Лучше подгонять параметры α,β и размеры сетки в зависимости от разрешения и разностью между ближней и дальней плоскостью отсечения. В целом, значения α = 5.0,β = 1.0 / (eα - 1.0) при размерах сетки от 128x768 дают приемлемый результат.
Ещё одной проблемой проекционной сетки является наличие артефактов по краям, возникающих при сдвиге вершин спроецированной сетки, используя карту высот и сдвигов, для создания остроконечного профиля волн.
Очевидным решением является расширение проекционной сетки, чтобы сдвинутые краевые вершины оставались за пределами экрана. Но сетку в таком случае придется либо очень сильно расширять, либо реализовывать достаточно сложную схему выбора величины сдвига в зависимости от положения наблюдателя относительно поверхности воды, амплитуды волн, расстояния до края сетки.
Разработчики CryEngine 2 предлагают достаточно простой подход для решения этой проблемы: уменьшать амплитуду волн по краям. Это достаточно простой и эффективный способ, но лучше вдобавок к нему ещё немного растягивать спроецированную сетку, чтобы уменьшение амплитуды волн по краям было менее заметным.
Вычисление ДПФ можно производить только с использованием вещественной текстуры, использование других форматов не дает достаточной точности. Тут возникает проблема: видеокарты уровня Direct3D 9, если и поддерживают фильтрацию вещественных текстур, то только fp16. С другой стороны, эти же видеокарты в вершинном шейдере могут выбирать только из текстур 32 битной точности (и то, как правило, не всех).
Некоторые решения данной проблемы:
Стоит заметить, что после того как преобразование Фурье произведено, карту высот и нормалей можно перерисовать в обычные текстуры и успользовать уже их для визуализации поверхности. Это снизит суммарное колличество выбираемой памяти. Видеокарты уровная Direct3D 10 могу использовать любые форматы текстур в вершинном шейдере. Выборки же из карты нормалей можно делать в фрагментном шейдере.
Деформированная поверхность воды усложняет использование описанных алгоритмов для генерации отражений и преломлений, потому что возникает проблема определения фрагментов попавших под воду (на воду), которые не нужно рисовать в карту отражений (соответсвенно преломлений), из-за этого могут быть видны артефакты стыковки отражений(преломлений) и видимых объектов в точке их соприкосновения с водой.
Можно модифицировать шейдеры, используемые при рендеринге в текстуру отражений, чтобы они отсекали только то, что действительно под водой, но при деформации текстурной координаты нормалью, все равно можно получить "нереальное" отражение.
Без деформации нормалью тоже плохо, хотя и смещение точек водной поверхности дает изломанное отражение, выглядит оно не очень реалистично. Получается, что океан больше похож на покрывало колыхающееся на ветру.
Во многих приложениях, проблемы с отражениями и преломлениями решаются уменьшением амплитуды волн и соответсвенно уменьшением силы деформации, при таких условиях проблема ограничивается, просто потому что баг труднозаметен.
Можно пойти и другим путем(если вам все-таки требуются большие волны и сильно искаженные отражения и преломления) - наоборот, деформировать и размыть всё сильнее, чтобы опять же, баг было труднее заметить. Конечно, оба решения косметические, но в конце концов, они дают приемлемый результат. Для пущего размытия можно вместо обычной выборки из текстуры отражений делать выборку не из старшего mipmap уровня(сделать это можно командой GLSL texture2DLod). Разумеется перед отрисовкой воды нужно сгенерировать mipmap уровни(в OpenGL функция glGenerateMipmapEXT):
Корректное отображение преломлений несколько более трудная задача. Во-первых, потому что вблизи объектов они видны лучше, по большому счету, вблизи берега отражений вообще почти не видно - просвечивает дно, и проблемы стыковки отражений и объектов достаточно трудно увидеть. Во-вторых, преломления более четкие, поэтому решение проблемы размытием выглядит неестественно. Также, желательно учитывать толщу воды, ослабляющую видимость.
В случае с отражениями, использовалась плоскость отсечения для отбраковки объектов, которые недолжны отражаться. Для преломлений можно предложить более точное решение. Нужно сравнивать расстоянием до объекта с расстоянием до поверхности воды. Если первое больше, то объект находится под водой и преломления учитывать нужно, в противном случае искажать преломления не надо. После такой коррекции ещё остаются проблемы как на рисунке 19 или 23 справа. Проблемы стыковки, как уже говорилось, обычно не исправляют, но ограничивают. А коррекцию по краям можно сделать уменьшая силу искажения в зависимости от расстояния до рамки окна, примерно так как это делалось в случае с амплитудой волн при использовании проекционной сетки.
Далее рассмотрен шейдер для визуализации океана, работающий при следующих условиях, по крайней мере иногда:
/=============== Vertex Shader ===============/ // Карта полученная с помощью двух FFT: для вычисления аплитды волн, для придания волнам острого профиля. // Первые две компоненты - (высота, фаза), вторые две - смещение для придания остроугольности. uniform sampler2D heightMap; uniform float sharpness; // величина, характеризующая остроту волн uniform vec2 surfaceSize; // размер просчитанного сегмента поверхности uniform vec3 surfaceCorners[4]; // четырехугльник, полученный при пересечении с пирамидой видимости uniform vec4 lightPosition; // позиция источника света в мировых координатах uniform vec4 eyePosition; // позиция наблюдателя в мировых координатах uniform mat4 projectionMatrix; uniform mat4 worldViewMatrix; uniform mat4 reflectionMatrix; // матрица для вычисления текстурных координат в карте отражений varying vec2 normalTexCoord; varying vec2 reflectionTexCoord; varying vec3 eyeDir; varying vec3 lightDir; varying vec3 fragmentPosition; void main() { // Вычисляем абсолютную позицию точки внутри четырехугольника vec3 a = (surfaceCorners[2] - surfaceCorners[3]) * gl_Vertex.y; vec3 b = (surfaceCorners[1] - surfaceCorners[0]) * gl_Vertex.y; vec3 c = mix(b, a, gl_Vertex.x); vec3 d = (surfaceCorners[3] - surfaceCorners[0]) * gl_Vertex.x; vec4 waterVertex = vec4(surfaceCorners[0] + c + d, 1.0); // Вычисляем текстурную координату для нормали и амплитуды normalTexCoord = waterVertex.xz / surfaceSize; // Сдвигаем вершину, используя карту высот и смещения vec4 texel = texture2D(heightMap, normalTexCoord); float attenuation = min( pow(1.0 - gl_Vertex.y, 0.2), pow(1.0 - abs(gl_Vertex.x - 0.5) * 2.0, 0.2) ); waterVertex.y = texel.x * attenuation; waterVertex.xz -= sharpness * texel.zw * attenuation; // Вектор от наблюдателя(источника света) в точку океана eyeDir = waterVertex.xyz - eyePosition.xyz; lightDir = waterVertex.xyz - lightPosition.xyz; // Вычисление текстурной координаты для отражений, // её деформацию производится в фрагментном шейдере, чтобы // не делать лишних выборок в вершинном. vec4 projTexCoord = reflectionMatrix * waterVertex; reflectionTexCoord = projTexCoord.xy / projTexCoord.w; gl_Position = projectionMatrix * worldViewMatrix * waterVertex; // Вычисляем нормализованную позиция фрагмента. В третьей компоненте расстояние до точки fragmentPosition = vec3(gl_Position.xy / gl_Position.w, gl_Position.w); } /=============== Fragment Shader =============/ uniform sampler2D depthMap; // Текстура глубины, используемая при рендеринге преломлений uniform sampler2D normalMap; // Предрассчитанная текстура с нормалями для карты высот uniform sampler2D reflectMap; // Текстура с отражениями uniform sampler2D refractMap; // Текстура с преломлениями uniform samplerCube environmentMap; // Текстура окражения(skybox) uniform float distanceAttenuation; // Величина характеризующая скорость затуманивания в зависимоти от дальности uniform float waterTransparency; // Прозрачность воды uniform vec4 fogColor; // Цвет тумана uniform vec4 lightSpecular; // Увет источника света // Нижний правый угол инвертированной песпективной матрицы. // Матрица нужна для восстановления расстояния до точки по значению в текстуре глдубины. // Нужен только угол размера 2х2, потому что есть необходимость только в последних // двух компонентах для определения расстояния до точки. uniform mat2 projectionMatrixInverse; varying vec2 normalTexCoord; varying vec2 reflectionTexCoord; varying vec3 eyeDir; varying vec3 lightDir; varying vec3 fragmentPosition; // Получить глубину точки по значению в карте глубин. // fragmentPosition - позиция фрагмента в нормализованных экранных координатах. // texCoord - текстурная координата для выборки из текстуры глубин. float get_fragment_depth(sampler2D depthMap, vec2 fragmentPosition, vec2 texCoord) { float depth = 2.0 * texture2D(depthMap, texCoord).r - 1.0; vec2 vec = projectionMatrixInverse * vec2(depth, 1.0); return -vec.x / vec.y; } void main() { const vec3 waterMinColor = vec3(0.0, 0.05, 0.15); const vec3 waterMaxColor = vec3(0.0, 0.1, 0.15); // Нормализованное направление на точку и нормаль в мировых координатах vec3 eyeDirNorm = normalize(eyeDir); vec3 normalNorm = 2.0 * ( texture2D(normalMap, normalTexCoord).rgb - vec3(0.5) ); // Нормаль будем выпрямлять вдали, чтобы поверхность выглядела менее периодичной float distVal = exp(-fragmentPosition.z * distanceAttenuation); normalNorm = mix( vec3(0.0, 1.0, 0.0), normalNorm, pow(distVal, 10.0) ); #ifdef ENABLE_LIGHTING // Рассчет освещения по модели Блинна-Фонга vec3 lightDirNorm = normalize(lightDir); vec3 halfVecNorm = normalize(eyeDirNorm + lightDirNorm); vec4 light = lightSpecular * pow( abs( dot(halfVecNorm, normalNorm) ), 16.0); // Ослабляем освещенность вдали, так выглядит естественней gl_FragColor = light * pow(distVal, 10.0); #else // Меняем цвет воды в зависимоти от угла обзора. float dotValue = dot(eyeDirNorm, normalNorm); vec3 waterColor = mix( waterMinColor, waterMaxColor, abs(dotValue) ); // Вычисляем текстурную координату, соответсвующую позиции фрагмента vec2 fragTexCoord = 0.5 * fragmentPosition.xy + vec2(0.5); #ifdef ENABLE_REFRACTIONS { // Деформируем текстурную координату для преломлений vec2 distortTexCoord = fragTexCoord + normalNorm.xz * 0.02; #ifdef ENABLE_DEPTH_MAP float nonDistortDepth = get_fragment_depth(depthMap, fragmentPosition.xy, fragTexCoord); float distortDepth = get_fragment_depth(depthMap, fragmentPosition.xy, distortTexCoord); // Защищаем от деформации объекты над поверхностью воды float deltaDepth; if (distortDepth > fragmentPosition.z) { deltaDepth = distortDepth - fragmentPosition.z; } else { distortTexCoord = fragTexCoord; deltaDepth = nonDistortDepth - fragmentPosition.z; } // Вычисляем ослабление в зависимости от глубины. Можно использовать и экспоненту float depthAttenuation = 1.0 / pow(1.0 + deltaDepth, 1.0 / waterTransparency - 1.0); // Смешиваем преломления с цветом воды vec4 refractColor = texture2D(refractMap, distortTexCoord).rgba; waterColor = mix(waterColor, refractColor.rgb, depthAttenuation * refractColor.a); #else vec4 refractColor = texture2D(refractMap, distortTexCoord).rgba; waterColor = mix(waterColor, refractColor.rgb, waterTransparency * refractColor.a); #endif } #endif // Вычисляем коэффициенты Френеля float fresnel = clamp( pow(1.0 + dotValue, 4.0), 0.05, 0.3 ); vec3 reflectColor = textureCube( environmentMap, reflect(eyeDirNorm, normalNorm) ).rgb; #ifdef ENABLE_REFLECTIONS { // Добавляем локальные отражения vec2 distortTexCoord = reflectionTexCoord + normalNorm.xz * 0.05; vec4 localReflectColor = texture2DLod(reflectMap, distortTexCoord, 2.0).rgba; reflectColor = mix(reflectColor, localReflectColor.rgb, localReflectColor.a); } #endif // Смешиваем все waterColor = mix(waterColor, reflectColor, fresnel); waterColor = mix(fogColor.rgb, waterColor, distVal); gl_FragColor = vec4(waterColor, 1.0); #endif }
Пожарной сигнализации Интеграции пожарной сигнализации. sio.su |