
Nos artigos anteriores, usamos iluminação direta (renderização direta ou sombreamento direto) . Essa é uma abordagem simples na qual desenhamos um objeto levando em consideração todas as fontes de luz, depois desenhamos o próximo objeto juntamente com toda a iluminação nele, e assim por diante para cada objeto. É muito simples de entender e implementar, mas, ao mesmo tempo, ocorre lentamente do ponto de vista do desempenho: para cada objeto, você precisa classificar todas as fontes de luz. Além disso, a iluminação direta funciona ineficientemente em cenas com um grande número de objetos sobrepostos, uma vez que a maioria dos cálculos do pixel shader não será útil e será substituída por valores para objetos mais próximos.
A iluminação adiada, o sombreamento adiado ou a renderização adiada contornam esse problema e alteram drasticamente a maneira como desenhamos objetos. Isso oferece novas oportunidades para otimizar significativamente as cenas com um grande número de fontes de luz, permitindo desenhar centenas e até milhares de fontes de luz a uma velocidade aceitável. Abaixo, uma cena com fontes de luz pontuais de 1847, desenhadas usando iluminação diferida (imagem cortesia de Hannes Nevalainen). Algo assim seria impossível com um cálculo direto da iluminação:

A idéia de iluminação diferida é que adiamos as partes mais complexas computacionalmente (como iluminação) para mais tarde. A iluminação diferida consiste em duas passagens: na primeira, a passagem da geometria (passagem da geometria) , toda a cena é desenhada e várias informações são armazenadas em um conjunto de texturas chamado buffer G. Por exemplo: posições, cores, normais e / ou espelhamento de superfície para cada pixel. As informações gráficas armazenadas no buffer G são usadas posteriormente para calcular a iluminação. A seguir, o conteúdo do buffer G para um quadro:

Na segunda passagem, chamada passagem de iluminação, usamos as texturas do buffer G quando desenhamos o retângulo em tela cheia. Em vez de usar sombreadores de vértice e fragmento separadamente para cada objeto, desenhamos a cena inteira pixel por pixel. O cálculo da iluminação permanece exatamente o mesmo que com uma passagem direta, mas coletamos os dados necessários apenas do buffer G e dos sombreadores variáveis (uniformes) , e não do sombreamento de vértice.
A imagem abaixo mostra bem o processo geral de desenho.

A principal vantagem é que as informações armazenadas no buffer G pertencem aos fragmentos mais próximos que não são obscurecidos por nada: o teste de profundidade os deixa apenas. Graças a isso, calculamos a iluminação de cada pixel apenas uma vez, sem fazer muito trabalho. Além disso, a iluminação diferida oferece oportunidades para otimizações adicionais, permitindo o uso de muito mais fontes de luz do que na iluminação direta.
No entanto, existem algumas desvantagens: o buffer G armazena uma grande quantidade de informações sobre a cena. Além disso, os dados do tipo posição devem ser armazenados com alta precisão, como resultado, o buffer G ocupa bastante espaço na memória. Outra desvantagem é que não poderemos usar objetos translúcidos (uma vez que o buffer armazena informações apenas para a superfície mais próxima) e o recurso anti-aliasing, como o MSAA, também não funcionará. Existem várias soluções alternativas para resolver esses problemas, elas são discutidas no final do artigo.
(Faixa de nota. - O buffer G ocupa muito espaço de memória. Por exemplo, para uma tela de 1920 * 1080 e usando 128 bits por pixel, o buffer terá 33mb. Existem requisitos crescentes para a largura de banda da memória - há muito mais dados sendo gravados e lidos)
Buffer G
O buffer G refere-se às texturas usadas para armazenar informações relacionadas à iluminação usadas no último passo de renderização. Vamos ver quais informações precisamos para calcular a iluminação para renderização direta:
- Vetor de posição 3D: usado para descobrir a posição do fragmento em relação à câmera e às fontes de luz.
- Cor difusa do fragmento (refletividade para vermelho, verde e azul - em geral, cor).
- Vetor 3d normal (para determinar em que ângulo a luz cai na superfície)
- flutuar para armazenar o componente espelho
- A posição da fonte de luz e sua cor.
- Posição da câmera.
Usando essas variáveis, podemos calcular a cobertura usando o modelo de Blinn-Fong que já conhecemos. A cor e a posição da fonte de luz, bem como a posição da câmera, podem ser variáveis comuns, mas o restante dos valores será diferente para cada fragmento de imagem. Se passarmos exatamente os mesmos dados para a passagem final da iluminação diferida, que usaríamos para uma passagem direta, obteremos o mesmo resultado, apesar de desenharmos fragmentos em um retângulo 2D comum.
O OpenGL não possui restrições quanto ao que podemos armazenar em uma textura, portanto, faz sentido armazenar todas as informações em uma ou mais texturas do tamanho de uma tela (chamada de buffer G) e usá-las no passe de iluminação. Como o tamanho das texturas e da tela é o mesmo, obtemos os mesmos dados de entrada da iluminação direta.
No pseudo-código, a imagem geral é mais ou menos assim:
while(...) // render loop { // 1. : / g- glBindFramebuffer(GL_FRAMEBUFFER, gBuffer); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); gBufferShader.use(); for(Object obj : Objects) { ConfigureShaderTransformsAndUniforms(); obj.Draw(); } // 2. : g- glBindFramebuffer(GL_FRAMEBUFFER, 0); glClear(GL_COLOR_BUFFER_BIT); lightingPassShader.use(); BindAllGBufferTextures(); SetLightingUniforms(); RenderQuad(); }
Informações necessárias para cada pixel: vetor de posição , vetor normal , vetor de cores e valor para o componente de espelho . No passo geométrico, desenhamos todos os objetos na cena e salvamos todos esses dados no buffer G. Podemos usar vários destinos de renderização para preencher todos os buffers de uma só vez. Essa abordagem foi discutida no artigo anterior sobre a implementação do brilho: Bloom , tradução no hub
Para a passagem geométrica, crie um framebuffer com o nome óbvio gBuffer, ao qual anexaremos vários buffers de cores e um buffer de profundidade. Para armazenar posições e normais, é preferível usar uma textura com alta precisão (valores flutuantes de 16 ou 32 bits para cada componente); por padrão, armazenaremos os valores especulares e de cor difusa na textura (precisão de 8 bits por componente).
unsigned int gBuffer; glGenFramebuffers(1, &gBuffer); glBindFramebuffer(GL_FRAMEBUFFER, gBuffer); unsigned int gPosition, gNormal, gColorSpec; // glGenTextures(1, &gPosition); glBindTexture(GL_TEXTURE_2D, gPosition); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB16F, SCR_WIDTH, SCR_HEIGHT, 0, GL_RGB, GL_FLOAT, NULL); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, gPosition, 0); // glGenTextures(1, &gNormal); glBindTexture(GL_TEXTURE_2D, gNormal); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB16F, SCR_WIDTH, SCR_HEIGHT, 0, GL_RGB, GL_FLOAT, NULL); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT1, GL_TEXTURE_2D, gNormal, 0); // + glGenTextures(1, &gAlbedoSpec); glBindTexture(GL_TEXTURE_2D, gAlbedoSpec); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, SCR_WIDTH, SCR_HEIGHT, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT2, GL_TEXTURE_2D, gAlbedoSpec, 0); // OpenGL, unsigned int attachments[3] = { GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1, GL_COLOR_ATTACHMENT2 }; glDrawBuffers(3, attachments); // . [...]
Como usamos vários objetivos de renderização, devemos informar explicitamente ao OpenGL a quais buffers do GBuffer anexados vamos desenhar glDrawBuffers()
. Também é importante notar que armazenamos posições e normais têm 3 componentes cada e as armazenamos em texturas RGB. Mas, ao mesmo tempo, imediatamente colocamos na mesma textura RGBA a cor e o coeficiente de reflexão especular - graças a isso usamos um buffer a menos. Se sua implementação da renderização adiada se tornar mais complexa e usar mais dados, você poderá encontrar facilmente novas maneiras de combinar dados e organizá-los em texturas.
No futuro, devemos renderizar os dados para o buffer G. Se cada objeto tiver coeficiente de reflexão de cor, normal e especular, podemos escrever algo como o seguinte sombreador:
#version 330 core layout (location = 0) out vec3 gPosition; layout (location = 1) out vec3 gNormal; layout (location = 2) out vec4 gAlbedoSpec; in vec2 TexCoords; in vec3 FragPos; in vec3 Normal; uniform sampler2D texture_diffuse1; uniform sampler2D texture_specular1; void main() { // G- gPosition = FragPos; // G- gNormal = normalize(Normal); // gAlbedoSpec.rgb = texture(texture_diffuse1, TexCoords).rgb; // gAlbedoSpec.a = texture(texture_specular1, TexCoords).r; }
Como usamos vários objetivos de renderização, com a ajuda do layout
indicamos o que e em que buffer do buffer de quadro atual renderizamos. Observe que não armazenamos o coeficiente de espelho em um buffer separado, pois podemos armazenar o valor flutuante no canal alfa de um dos buffers.
Lembre-se de que, ao calcular a iluminação, é extremamente importante armazenar todas as variáveis no mesmo espaço de coordenadas; nesse caso, armazenamos (e realizamos cálculos) no espaço do mundo.
Se agora renderizarmos vários nanosuits em um buffer G e desenhar seu conteúdo projetando cada buffer em um quarto da tela, veremos algo como isto:

Tente visualizar a posição e os vetores normais e verifique se eles estão corretos. Por exemplo, os vetores normais apontando para a direita serão vermelhos. Da mesma forma, com objetos localizados à direita do centro da cena. Depois de ficar satisfeito com o conteúdo do buffer G, vamos para a próxima parte: a passagem da iluminação.
Passagem de iluminação
Agora que temos uma grande quantidade de informações no buffer G, podemos calcular completamente a iluminação e as cores finais de cada pixel do buffer G, usando seu conteúdo como entrada para os algoritmos de cálculo da iluminação. Como os valores do buffer G representam apenas fragmentos visíveis, executaremos cálculos complexos de iluminação exatamente uma vez para cada pixel. Devido a isso, a iluminação diferida é bastante eficaz, especialmente em cenas complexas, nas quais, ao renderizar diretamente para cada pixel, muitas vezes é necessário calcular a iluminação várias vezes.
Para a passagem da iluminação, renderizamos um retângulo em tela cheia (um pouco como o efeito de pós-processamento) e executamos um cálculo lento da iluminação para cada pixel.
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, gPosition); glActiveTexture(GL_TEXTURE1); glBindTexture(GL_TEXTURE_2D, gNormal); glActiveTexture(GL_TEXTURE2); glBindTexture(GL_TEXTURE_2D, gAlbedoSpec); // shaderLightingPass.use(); SendAllLightUniformsToShader(shaderLightingPass); shaderLightingPass.setVec3("viewPos", camera.Position); RenderQuad();
Vinculamos todas as texturas necessárias do buffer G antes da renderização e, além disso, definimos os valores das variáveis relacionadas à iluminação no shader.
O sombreador de passagem de fragmentos é muito semelhante ao que usamos nas lições da reunião. Fundamentalmente nova é a maneira como obtemos informações para a iluminação diretamente do buffer G.
#version 330 core out vec4 FragColor; in vec2 TexCoords; uniform sampler2D gPosition; uniform sampler2D gNormal; uniform sampler2D gAlbedoSpec; struct Light { vec3 Position; vec3 Color; }; const int NR_LIGHTS = 32; uniform Light lights[NR_LIGHTS]; uniform vec3 viewPos; void main() { // G- vec3 FragPos = texture(gPosition, TexCoords).rgb; vec3 Normal = texture(gNormal, TexCoords).rgb; vec3 Albedo = texture(gAlbedoSpec, TexCoords).rgb; float Specular = texture(gAlbedoSpec, TexCoords).a; // vec3 lighting = Albedo * 0.1; // vec3 viewDir = normalize(viewPos - FragPos); for(int i = 0; i < NR_LIGHTS; ++i) { // vec3 lightDir = normalize(lights[i].Position - FragPos); vec3 diffuse = max(dot(Normal, lightDir), 0.0) * Albedo * lights[i].Color; lighting += diffuse; } FragColor = vec4(lighting, 1.0); }
O sombreador de iluminação aceita 3 texturas que contêm todas as informações registradas na passagem geométrica e das quais o buffer G consiste. Se usarmos a entrada para iluminação de texturas, obteremos exatamente os mesmos valores como se estivéssemos na renderização direta normal. No início do shader de fragmento, obtemos os valores relacionados às variáveis de iluminação simplesmente lendo a textura. Observe que obtemos a cor e o coeficiente de reflexão especular de uma textura - gAlbedoSpec
.
Como para cada fragmento existem valores (bem como variáveis de sombreamento uniformes) necessários para o cálculo da iluminação de acordo com o modelo de Blinn-Fong, não precisamos alterar o código de cálculo da iluminação. A única coisa que foi alterada é a maneira de obter os valores de entrada.
Iniciar uma demonstração simples com 32 pequenas fontes de luz é algo como isto:

Uma das desvantagens da iluminação diferida é a impossibilidade de misturar, pois todos os buffers-g para cada pixel contêm informações sobre apenas uma superfície, enquanto a mistura usa combinações de vários fragmentos. (Mistura) , tradução . Outra desvantagem da iluminação diferida é que ela obriga a usar um método comum para calcular a iluminação de todos os objetos; embora essa limitação possa de alguma forma ser contornada adicionando informações materiais ao buffer g.
Para lidar com essas deficiências (especialmente a falta de mistura), elas geralmente dividem a renderização em duas partes: renderização com iluminação diferida e a segunda parte com renderização direta destinada a aplicar algo à cena ou usar shaders que não são compatíveis com a iluminação diferida. (Nota dos exemplos: adição de fumaça translúcida, fogo, vidro) Para ilustrar o trabalho, desenharemos as fontes de luz como cubos pequenos usando renderização direta, pois os cubos de iluminação exigem um sombreador especial (eles brilham uniformemente na mesma cor).
Combine renderização adiada com direta.
Suponha que desejemos desenhar cada fonte de luz na forma de um cubo 3D com um centro coincidindo com a posição da fonte de luz e emitindo luz com a cor da fonte. A primeira idéia que vem à mente é renderizar cubos diretamente para cada fonte de luz sobre os resultados da renderização adiada. Ou seja, desenhamos cubos como de costume, mas somente após a renderização adiada. O código será algo parecido com isto:
// [...] RenderQuad(); // shaderLightBox.use(); shaderLightBox.setMat4("projection", projection); shaderLightBox.setMat4("view", view); for (unsigned int i = 0; i < lightPositions.size(); i++) { model = glm::mat4(); model = glm::translate(model, lightPositions[i]); model = glm::scale(model, glm::vec3(0.25f)); shaderLightBox.setMat4("model", model); shaderLightBox.setVec3("lightColor", lightColors[i]); RenderCube(); }
Esses cubos renderizados não levam em consideração os valores de profundidade da renderização adiada e, como resultado, são sempre desenhados sobre objetos já renderizados: não é isso que estamos buscando.

Primeiro, precisamos copiar as informações de profundidade da passagem geométrica para o buffer de profundidade e somente depois desenhar os cubos luminosos. Assim, fragmentos de cubos luminosos serão desenhados apenas se estiverem mais próximos do que objetos já desenhados.
Podemos copiar o conteúdo do framebuffer para outro framebuffer usando a função glBlitFramebuffer
. Já usamos essa função no exemplo de anti-aliasing : ( anti-aliasing ), tradução . A função glBlitFramebuffer
copia a parte especificada pelo usuário do framebuffer para a parte especificada de outro framebuffer.
Para objetos desenhados na passagem de iluminação adiada, salvamos a profundidade no buffer g do objeto buffer de estrutura. Se simplesmente copiarmos o conteúdo do buffer de profundidade do buffer g para o buffer de profundidade padrão, os cubos luminosos serão desenhados como se toda a geometria da cena fosse desenhada usando uma passagem de renderização direta. Como foi explicado brevemente no exemplo de suavização de serrilhado, precisamos definir buffers de quadros para leitura e gravação:
glBindFramebuffer(GL_READ_FRAMEBUFFER, gBuffer); glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); // - glBlitFramebuffer( 0, 0, SCR_WIDTH, SCR_HEIGHT, 0, 0, SCR_WIDTH, SCR_HEIGHT, GL_DEPTH_BUFFER_BIT, GL_NEAREST ); glBindFramebuffer(GL_FRAMEBUFFER, 0); // [...]
Aqui, copiamos todo o conteúdo do buffer de profundidade do buffer de quadros para o buffer de profundidade padrão (se necessário, você pode copiar os buffers de cores ou o buffer stensil da mesma maneira). Se agora renderizarmos os cubos brilhantes, eles serão desenhados como se a geometria da cena fosse real (embora seja desenhada como simples).

O código fonte da demonstração pode ser encontrado aqui .
Com essa abordagem, podemos combinar facilmente a renderização adiada com a renderização direta. Isso é excelente, pois podemos aplicar objetos de mesclagem e desenho que requerem sombreadores especiais que não são aplicáveis à renderização adiada.
Mais fontes de luz
A iluminação diferida é frequentemente elogiada por ser capaz de atrair um grande número de fontes de luz sem uma diminuição significativa no desempenho. A iluminação atrasada sozinha não nos permite desenhar um número muito grande de fontes de luz, pois ainda precisamos calcular a contribuição de todas as fontes de luz para cada pixel. Para desenhar um grande número de fontes de luz, é usada uma otimização muito bonita, aplicável à renderização diferida - a área de ação das fontes de luz. (volumes leves)
Geralmente, quando desenhamos fragmentos em uma cena altamente iluminada, levamos em consideração a contribuição de cada fonte de luz na cena, independentemente da distância do fragmento. Se a maioria das fontes de luz nunca afetará o fragmento, por que estamos perdendo tempo computando para elas?
A idéia do escopo da fonte de luz é encontrar o raio (ou volume) da fonte de luz - ou seja, a área na qual a luz é capaz de alcançar a superfície. Como a maioria das fontes de luz usa algum tipo de atenuação, podemos encontrar a distância máxima (raio) que a luz pode alcançar. Depois disso, realizamos cálculos complexos de iluminação apenas para as fontes de luz que afetam esse fragmento. Isso nos poupa de um grande número de cálculos, pois só calculamos a iluminação onde é necessária.
Com essa abordagem, o principal truque é determinar o tamanho da área de ação da fonte de luz.
Cálculo do escopo de uma fonte de luz (raio)
Para obter o raio da fonte de luz, precisamos resolver a equação de amortecimento do brilho, que consideramos escuro - pode ser 0,0 ou algo um pouco mais iluminado, mas ainda escuro: por exemplo, 0,03. Para demonstrar como calcular o raio, usaremos uma das funções de atenuação mais complexas e comuns do exemplo do caster de luz
Queremos resolver esta equação para o caso em que , ou seja, quando a fonte de luz estiver completamente escura. No entanto, essa equação nunca atingirá o valor exato de 0,0, portanto não há solução. No entanto, podemos resolver a equação do brilho para um valor próximo a 0,0, que pode ser considerado praticamente escuro. Neste exemplo, consideramos aceitável o valor do brilho em - dividido por 256, pois o buffer de quadros de 8 bits pode conter 256 valores de brilho diferentes.
A função de atenuação selecionada fica quase escura a uma distância do alcance; se a limitarmos a um brilho menor que 5/256, o alcance da fonte de luz ficará muito grande - isso não é tão eficaz. Idealmente, uma pessoa não deve ver uma borda repentina e nítida de luz de uma fonte de luz. Obviamente, isso depende do tipo de cena, um valor maior do brilho mínimo fornece áreas menores de ação das fontes de luz e aumenta a eficiência dos cálculos, mas pode levar a artefatos visíveis na imagem: a iluminação interrompe abruptamente nas bordas da área de ação da fonte de luz.
A equação de atenuação que devemos resolver se torna:
Aqui - o componente mais brilhante da luz (dos canais r, g, b). Usaremos o componente mais brilhante, pois os outros componentes darão uma restrição mais fraca ao escopo da fonte de luz.
Continuamos a resolver a equação:
A última equação é uma equação quadrática na forma com a seguinte solução:
Temos uma equação geral que nos permite substituir os parâmetros (atenuação constante, coeficientes linear e quadrático) para encontrar x - o raio da fonte de luz.
float constant = 1.0; float linear = 0.7; float quadratic = 1.8; float lightMax = std::fmaxf(std::fmaxf(lightColor.r, lightColor.g), lightColor.b); float radius = (-linear + std::sqrtf(linear * linear - 4 * quadratic * (constant - (256.0 / 5.0) * lightMax))) / (2 * quadratic);
A fórmula retorna um raio entre aproximadamente 1,0 e 5,0, dependendo do brilho máximo da fonte de luz.
Encontramos esse raio para cada fonte de luz no palco e o usamos para levar em conta apenas as fontes de luz nas quais ele está localizado no escopo de cada fragmento. Abaixo está uma passagem refeito da iluminação que leva em consideração as áreas de ação das fontes de luz. Observe que essa abordagem é implementada apenas para fins educacionais e não é adequada para uso prático (em breve discutiremos o porquê).
struct Light { [...] float Radius; }; void main() { [...] for(int i = 0; i < NR_LIGHTS; ++i) { // float distance = length(lights[i].Position - FragPos); if(distance < lights[i].Radius) { // [...] } } }
O resultado é exatamente o mesmo de antes, mas agora para cada fonte de luz, seu efeito é levado em consideração apenas dentro da área de sua ação.
O código final é uma demonstração. .
A aplicação real do escopo da fonte de luz.
O sombreador de fragmentos mostrado acima não funcionará na prática e serve apenas para ilustrar como podemos nos livrar de cálculos desnecessários de iluminação. Na realidade, a placa de vídeo e a linguagem do shader GLSL otimizam muito mal os loops e ramificações. A razão para isso é que a execução do shader na placa de vídeo é realizada em paralelo para diferentes pixels, e muitas arquiteturas impõem a limitação de que, na execução paralela, diferentes threads devem calcular o mesmo shader. Muitas vezes, isso leva ao fato de que o shader em execução sempre calcula todos os ramos, para que todos os shaders funcionem ao mesmo tempo. (Nota: isso não afeta o resultado dos cálculos, mas pode reduzir o desempenho do sombreador.) Por causa disso, pode ser que nossa verificação do raio seja inútil: ainda calcularemos a iluminação para todas as fontes!
Uma abordagem adequada para usar o escopo da luz é renderizar esferas com um raio como o de uma fonte de luz. O centro da esfera coincide com a posição da fonte de luz, de modo que a esfera contenha em si a faixa de ação da fonte de luz. Há um pequeno truque aqui - usamos basicamente o mesmo shader de fragmento adiado para desenhar uma esfera. Ao desenhar uma esfera, o sombreador de fragmento é chamado especificamente para os pixels afetados pela fonte de luz, renderizamos apenas os pixels necessários e pulamos todos os outros. :

, . , , . _*__
_ + __ , .
: ( ) , , , - ( ). stenil .
, , , . ( ) : c (deferred lighting) (tile-based deferred shading) . MSAA. .
vs
( ) - , , . , — , MSAA, .
( ), ( g- ..) . , .
: , , , . , , , . . parallax mapping, , . , .
Sitelinks
PS - . , !