Я пытаюсь создать свой собственный шейдер SSAO при прямом рендеринге (не при постобработке) с помощью GLSL. Я сталкиваюсь с некоторыми проблемами, но я действительно не могу понять, что не так с моим кодом.
Он создан с помощью движка Babylon JS как BABYLON.ShaderMaterial
и установлен как BABYLON.RenderTargetTexture
, и в основном он основан на этом известном руководстве по SSAO: http://john-chapman-graphics.blogspot.fr/2013/01/ssao-tutorial.html
Из соображений производительности я должен выполнять все расчеты без проецирования и депроецирования в пространстве экрана, я бы предпочел использовать метод луча просмотра, описанный в руководстве выше.
Прежде чем объяснять все это, обратите внимание, что Babylon JS использует левостороннюю систему координат, что может иметь место в моем коде.
Вот мои классические шаги:
- Во-первых, я вычисляю положения дальних углов четырех камер в своем JS-коде. Они могут быть постоянными каждый раз, поскольку они вычисляются в пространственном положении.
// Calculating 4 corners manually in view space
var tan = Math.tan;
var atan = Math.atan;
var ratio = SSAOSize.x / SSAOSize.y;
var far = scene.activeCamera.maxZ;
var fovy = scene.activeCamera.fov;
var fovx = 2 * atan(tan(fovy/2) * ratio);
var xFarPlane = far * tan(fovx/2);
var yFarPlane = far * tan(fovy/2);
var topLeft = new BABYLON.Vector3(-xFarPlane, yFarPlane, far);
var topRight = new BABYLON.Vector3( xFarPlane, yFarPlane, far);
var bottomRight = new BABYLON.Vector3( xFarPlane, -yFarPlane, far);
var bottomLeft = new BABYLON.Vector3(-xFarPlane, -yFarPlane, far);
var farCornersVec = [topLeft, topRight, bottomRight, bottomLeft];
var farCorners = [];
for (var i = 0; i < 4; i++) {
var vecTemp = farCornersVec[i];
farCorners.push(vecTemp.x, vecTemp.y, vecTemp.z);
}
Эти угловые позиции отправляются в вершинный шейдер, поэтому векторные координаты сериализуются в массиве
farCorners[]
для отправки в вершинный шейдер.В моем вершинном шейдере знаки
position.x
иposition.y
сообщают шейдеру, какой угол использовать при каждом проходе.Затем эти углы интерполируются в моем фрагментном шейдере для расчета луча обзора, то есть вектора от камеры до дальней плоскости (его компонент .z, таким образом, равен расстоянию от дальней плоскости до камеры).
Фрагментный шейдер следует инструкциям руководства Джона Чепмена (см. код с комментариями ниже).
Я получаю свой буфер глубины как BABYLON.RenderTargetTexture
с помощью метода DepthRenderer.getDepthMap()
. Поиск текстуры глубины фактически возвращает (согласно шейдерам глубины Babylon JS): (gl_FragCoord.z / gl_FragCoord.w) / far
, с:
gl_FragCoord.z
: нелинейная глубинаgl_FragCoord.z = 1/Wc
, гдеWc
— позиция вершины в пространстве отсечения (т.е.gl_Position.w
в вершинном шейдере)far
: положительное расстояние от камеры до дальней плоскости.
Образцы ядра расположены в виде полусферы со случайными поплавками в [0,1], большинство из которых распределены близко к началу координат с помощью линейной интерполяции.
Поскольку у меня нет нормальной текстуры, я вычисляю их из текущего значения буфера глубины с помощью getNormalFromDepthValue()
:
vec3 getNormalFromDepthValue(float depth) {
vec2 offsetX = vec2(texelSize.x, 0.0);
vec2 offsetY = vec2(0.0, texelSize.y);
// texelSize = size of a texel = (1/SSAOSize.x, 1/SSAOSize.y)
float depthOffsetX = getDepth(depthTexture, vUV + offsetX); // Horizontal neighbour
float depthOffsetY = getDepth(depthTexture, vUV + offsetY); // Vertical neighbour
vec3 pX = vec3(offsetX, depthOffsetX - depth);
vec3 pY = vec3(offsetY, depthOffsetY - depth);
vec3 normal = cross(pY, pX);
normal.z = -normal.z; // We want normal.z positive
return normalize(normal); // [-1,1]
}
Наконец, моя функция getDepth()
позволяет мне получить значение глубины при текущем UV в 32-битном формате с плавающей запятой:
float getDepth(sampler2D tex, vec2 texcoord) {
return unpack(texture2D(tex, texcoord));
// unpack() retreives the depth value from the 4 components of the vector given by texture2D()
}
Вот мои коды вершинных и фрагментных шейдеров (без объявлений функций):
// ---------------------------- Vertex Shader ----------------------------
precision highp float;
uniform float fov;
uniform float far;
uniform vec3 farCorners[4];
attribute vec3 position; // 3D position of each vertex (4) of the quad in object space
attribute vec2 uv; // UV of each vertex (4) of the quad
varying vec3 vPosition;
varying vec2 vUV;
varying vec3 vCornerPositionVS;
void main(void) {
vPosition = position;
vUV = uv;
// Map current vertex with associated frustum corner position in view space:
// 0: top left, 1: top right, 2: bottom right, 3: bottom left
// This frustum corner position will be interpolated so that the pixel shader always has a ray from camera->far-clip plane.
vCornerPositionVS = vec3(0.0);
if (positionVS.x > 0.0) {
if (positionVS.y <= 0.0) { // top left
vCornerPositionVS = farCorners[0];
}
else if (positionVS.y > 0.0) { // top right
vCornerPositionVS = farCorners[1];
}
}
else if (positionVS.x <= 0.0) {
if (positionVS.y > 0.0) { // bottom right
vCornerPositionVS = farCorners[2];
}
else if (positionVS.y <= 0.0) { // bottom left
vCornerPositionVS = farCorners[3];
}
}
gl_Position = vec4(position * 2.0, 1.0); // 2D position of each vertex
}
// ---------------------------- Fragment Shader ----------------------------
precision highp float;
uniform mat4 projection; // Projection matrix
uniform float radius; // Scaling factor for sample position, by default = 1.7
uniform float depthBias; // 1e-5
uniform vec2 noiseScale; // (SSAOSize.x / noiseSize, SSAOSize.y / noiseSize), with noiseSize = 4
varying vec3 vCornerPositionVS; // vCornerPositionVS is the interpolated position calculated from the 4 far corners
void main() {
// Get linear depth in [0,1] with texture2D(depthBufferTexture, vUV)
float fragDepth = getDepth(depthBufferTexture, vUV);
float occlusion = 0.0;
if (fragDepth < 1.0) {
// Retrieve fragment's view space normal
vec3 normal = getNormalFromDepthValue(fragDepth); // in [-1,1]
// Random rotation: rvec.xyz are the components of the generated random vector
vec3 rvec = texture2D(randomSampler, vUV * noiseScale).rgb * 2.0 - 1.0; // [-1,1]
rvec.z = 0.0; // Random rotation around Z axis
// Get view ray, from camera to far plane, scaled by 1/far so that viewRayVS.z == 1.0
vec3 viewRayVS = vCornerPositionVS / far;
// Current fragment's view space position
vec3 fragPositionVS = viewRay * fragDepth;
// Creation of TBN matrix
vec3 tangent = normalize(rvec - normal * dot(rvec, normal));
vec3 bitangent = cross(normal, tangent);
mat3 tbn = mat3(tangent, bitangent, normal);
for (int i = 0; i < NB_SAMPLES; i++) {
// Get sample kernel position, from tangent space to view space
vec3 samplePosition = tbn * kernelSamples[i];
// Add VS kernel offset sample to fragment's VS position
samplePosition = samplePosition * radius + fragPosition;
// Project sample position from view space to screen space:
vec4 offset = vec4(samplePosition, 1.0);
offset = projection * offset; // To view space
offset.xy /= offset.w; // Perspective division
offset.xy = offset.xy * 0.5 + 0.5; // [-1,1] -> [0,1]
// Get current sample depth:
float sampleDepth = getDepth(depthTexture, offset.xy);
float rangeCheck = abs(fragDepth - sampleDepth) < radius ? 1.0 : 0.0;
// Reminder: fragDepth == fragPosition.z
// Range check and accumulate if fragment contributes to occlusion:
occlusion += (samplePosition.z - sampleDepth >= depthBias ? 1.0 : 0.0) * rangeCheck;
}
}
// Inversion
float ambientOcclusion = 1.0 - (occlusion / float(NB_SAMPLES));
ambientOcclusion = pow(ambientOcclusion, power);
gl_FragColor = vec4(vec3(ambientOcclusion), 1.0);
}
Размытие по Гауссу по горизонтали и вертикали впоследствии очищает шум, создаваемый случайной текстурой.
Мои параметры:
NB_SAMPLES = 16
radius = 1.7
depthBias = 1e-5
power = 1.0
Вот результат:
Результат имеет артефакты по краям, а близкие тени не очень сильные... Кто-нибудь увидит что-то неправильное или странное в моем коде?
Большое спасибо!