Parte 1. Zíperes
Nesta parte, veremos o processo de renderização de raios em Witcher 3: Wild Hunt.
A renderização de raio é realizada um pouco depois do efeito da
cortina de chuva , mas ainda ocorre no passe de renderização direta. Relâmpagos podem ser vistos neste vídeo:
Eles desaparecem muito rapidamente, por isso é melhor assistir ao vídeo a uma velocidade de 0,25.
Você pode ver que essas não são imagens estáticas; com o tempo, o brilho muda um pouco.
Em termos de renderização de nuances, há muitas semelhanças com o desenho de uma cortina de chuva à distância, por exemplo, os mesmos estados de mesclagem (mesclagem aditiva) e profundidade (a verificação é ativada, a gravação de profundidade não é executada).
Cena sem raioCena relâmpagoEm termos de geometria do raio, The Witcher 3 é uma malha semelhante a uma árvore. Este exemplo de raio é representado pela seguinte malha:
Possui coordenadas UV e vetores normais. Tudo isso é útil no estágio de vertex shader.
Sombreador de vértice
Vamos dar uma olhada no código do shader de vértices montado:
vs_5_0 dcl_globalFlags refactoringAllowed dcl_constantbuffer cb1[9], immediateIndexed dcl_constantbuffer cb2[6], immediateIndexed dcl_input v0.xyz dcl_input v1.xy dcl_input v2.xyz dcl_input v4.xyzw dcl_input v5.xyzw dcl_input v6.xyzw dcl_input v7.xyzw dcl_output o0.xy dcl_output o1.xyzw dcl_output_siv o2.xyzw, position dcl_temps 3 0: mov o0.xy, v1.xyxx 1: mov o1.xyzw, v7.xyzw 2: mul r0.xyzw, v5.xyzw, cb1[0].yyyy 3: mad r0.xyzw, v4.xyzw, cb1[0].xxxx, r0.xyzw 4: mad r0.xyzw, v6.xyzw, cb1[0].zzzz, r0.xyzw 5: mad r0.xyzw, cb1[0].wwww, l(0.000000, 0.000000, 0.000000, 1.000000), r0.xyzw 6: mov r1.w, l(1.000000) 7: mad r1.xyz, v0.xyzx, cb2[4].xyzx, cb2[5].xyzx 8: dp4 r2.x, r1.xyzw, v4.xyzw 9: dp4 r2.y, r1.xyzw, v5.xyzw 10: dp4 r2.z, r1.xyzw, v6.xyzw 11: add r2.xyz, r2.xyzx, -cb1[8].xyzx 12: dp3 r1.w, r2.xyzx, r2.xyzx 13: rsq r1.w, r1.w 14: div r1.w, l(1.000000, 1.000000, 1.000000, 1.000000), r1.w 15: mul r1.w, r1.w, l(0.000001) 16: mad r2.xyz, v2.xyzx, l(2.000000, 2.000000, 2.000000, 0.000000), l(-1.000000, -1.000000, -1.000000, 0.000000) 17: mad r1.xyz, r2.xyzx, r1.wwww, r1.xyzx 18: mov r1.w, l(1.000000) 19: dp4 o2.x, r1.xyzw, r0.xyzw 20: mul r0.xyzw, v5.xyzw, cb1[1].yyyy 21: mad r0.xyzw, v4.xyzw, cb1[1].xxxx, r0.xyzw 22: mad r0.xyzw, v6.xyzw, cb1[1].zzzz, r0.xyzw 23: mad r0.xyzw, cb1[1].wwww, l(0.000000, 0.000000, 0.000000, 1.000000), r0.xyzw 24: dp4 o2.y, r1.xyzw, r0.xyzw 25: mul r0.xyzw, v5.xyzw, cb1[2].yyyy 26: mad r0.xyzw, v4.xyzw, cb1[2].xxxx, r0.xyzw 27: mad r0.xyzw, v6.xyzw, cb1[2].zzzz, r0.xyzw 28: mad r0.xyzw, cb1[2].wwww, l(0.000000, 0.000000, 0.000000, 1.000000), r0.xyzw 29: dp4 o2.z, r1.xyzw, r0.xyzw 30: mul r0.xyzw, v5.xyzw, cb1[3].yyyy 31: mad r0.xyzw, v4.xyzw, cb1[3].xxxx, r0.xyzw 32: mad r0.xyzw, v6.xyzw, cb1[3].zzzz, r0.xyzw 33: mad r0.xyzw, cb1[3].wwww, l(0.000000, 0.000000, 0.000000, 1.000000), r0.xyzw 34: dp4 o2.w, r1.xyzw, r0.xyzw 35: ret
Existem muitas semelhanças com a cortina de chuva do vertex shader, então não vou repetir. Quero mostrar a diferença importante que está nas linhas 11 a 18:
11: add r2.xyz, r2.xyzx, -cb1[8].xyzx 12: dp3 r1.w, r2.xyzx, r2.xyzx 13: rsq r1.w, r1.w 14: div r1.w, l(1.000000, 1.000000, 1.000000, 1.000000), r1.w 15: mul r1.w, r1.w, l(0.000001) 16: mad r2.xyz, v2.xyzx, l(2.000000, 2.000000, 2.000000, 0.000000), l(-1.000000, -1.000000, -1.000000, 0.000000) 17: mad r1.xyz, r2.xyzx, r1.wwww, r1.xyzx 18: mov r1.w, l(1.000000) 19: dp4 o2.x, r1.xyzw, r0.xyzw
Em primeiro lugar, cb1 [8] .xyz é a posição da câmera e r2.xyz é a posição no espaço do mundo, ou seja, a linha 11 calcula o vetor da câmera para a posição no mundo. As linhas 12-15 calculam o
comprimento (worldPos - cameraPos) * 0.000001.v2.xyz é o vetor normal da geometria de entrada. A linha 16 a estende do intervalo [0-1] para o intervalo [-1; 1].
Então a posição final no mundo é calculada:
finalWorldPos = worldPos + comprimento (worldPos - cameraPos) * 0.000001 * normalVectorO trecho de código HLSL para esta operação será algo como isto:
...
Esta operação resulta em uma pequena “explosão” da malha (na direção do vetor normal). Eu experimentei substituindo 0,000001 por vários outros valores. Aqui estão os resultados:
0,0000020,0000050,000010,000025Pixel shader
Bem, nós descobrimos o shader de vértice, agora é hora de descer ao código do assembler para o shader de pixel!
ps_5_0 dcl_globalFlags refactoringAllowed dcl_constantbuffer cb0[1], immediateIndexed dcl_constantbuffer cb2[3], immediateIndexed dcl_constantbuffer cb4[5], immediateIndexed dcl_input_ps linear v0.x dcl_input_ps linear v1.w dcl_output o0.xyzw dcl_temps 1 0: mad r0.x, cb0[0].x, cb4[4].x, v0.x 1: add r0.y, r0.x, l(-1.000000) 2: round_ni r0.y, r0.y 3: ishr r0.z, r0.y, l(13) 4: xor r0.y, r0.y, r0.z 5: imul null, r0.z, r0.y, r0.y 6: imad r0.z, r0.z, l(0x0000ec4d), l(0.0000000000000000000000000000000000001) 7: imad r0.y, r0.y, r0.z, l(146956042240.000000) 8: and r0.y, r0.y, l(0x7fffffff) 9: round_ni r0.z, r0.x 10: frc r0.x, r0.x 11: add r0.x, -r0.x, l(1.000000) 12: ishr r0.w, r0.z, l(13) 13: xor r0.z, r0.z, r0.w 14: imul null, r0.w, r0.z, r0.z 15: imad r0.w, r0.w, l(0x0000ec4d), l(0.0000000000000000000000000000000000001) 16: imad r0.z, r0.z, r0.w, l(146956042240.000000) 17: and r0.z, r0.z, l(0x7fffffff) 18: itof r0.yz, r0.yyzy 19: mul r0.z, r0.z, l(0.000000001) 20: mad r0.y, r0.y, l(0.000000001), -r0.z 21: mul r0.w, r0.x, r0.x 22: mul r0.x, r0.x, r0.w 23: mul r0.w, r0.w, l(3.000000) 24: mad r0.x, r0.x, l(-2.000000), r0.w 25: mad r0.x, r0.x, r0.y, r0.z 26: add r0.y, -cb4[2].x, cb4[3].x 27: mad_sat r0.x, r0.x, r0.y, cb4[2].x 28: mul r0.x, r0.x, v1.w 29: mul r0.yzw, cb4[0].xxxx, cb4[1].xxyz 30: mul r0.xyzw, r0.xyzw, cb2[2].wxyz 31: mul o0.xyz, r0.xxxx, r0.yzwy 32: mov o0.w, r0.x 33: ret
Boas notícias: o código não é tão longo.
Más notícias:
3: ishr r0.z, r0.y, l(13) 4: xor r0.y, r0.y, r0.z 5: imul null, r0.z, r0.y, r0.y 6: imad r0.z, r0.z, l(0x0000ec4d), l(0.0000000000000000000000000000000000001) 7: imad r0.y, r0.y, r0.z, l(146956042240.000000) 8: and r0.y, r0.y, l(0x7fffffff)
... do que se trata?
Honestamente, essa não é a primeira vez que eu vejo uma peça desse tipo ... de código assembler nos shaders do Witcher 3. Mas quando o conheci pela primeira vez, pensei: "Que diabos é isso?"
Algo semelhante pode ser encontrado em alguns outros shaders TW3. Não descreverei minhas aventuras com esse fragmento e apenas digo que a resposta está no
ruído inteiro :
Como você pode ver, no pixel shader é chamado duas vezes. Usando os guias deste site, podemos entender como o ruído suave é implementado corretamente. Voltarei a isso em um minuto.
Veja a linha 0 - aqui estamos animando com base na seguinte fórmula:
animation = elapsedTime * animationSpeed + TextureUV.xEsses valores, após o arredondamento para o lado inferior (
piso ) (instrução
round_ni ) no futuro, se tornam pontos de entrada para ruído inteiro. Geralmente calculamos o valor do ruído para dois números inteiros e, em seguida, calculamos o valor final interpolado entre eles (consulte o site da libnoise para obter detalhes).
Bem, esse é
um ruído
inteiro , mas, afinal, todos os valores mencionados anteriormente (também arredondados para baixo) são flutuantes!
Observe que não há instruções
ftoi aqui . Suponho que os programadores da CD Projekt Red usaram a função interna HLSL
como aqui , que executa a conversão de valores de ponto flutuante "reinterpret_cast" e os trata como um padrão inteiro.
O peso de interpolação para os dois valores é calculado nas linhas 10-11.
interpolationWeight = 1.0 - frac (animação);Essa abordagem nos permite interpolar entre valores ao longo do tempo.
Para criar um ruído suave, esse interpolador é passado para a função
SCurve :
float s_curve( float x ) { float x2 = x * x; float x3 = x2 * x;
Função Smoothstep [libnoise.sourceforge.net]Esse recurso é conhecido como "passo suave". Porém, como você pode ver no código do assembler, essa
não é
uma função de
passo suave do HLSL. Uma função interna aplica restrições para que os valores sejam verdadeiros. Mas como sabemos que o
interpolationWeight sempre estará no intervalo [0-1], essas verificações podem ser ignoradas com segurança.
Ao calcular o valor final, várias operações de multiplicação são usadas. Veja como a saída alfa final pode mudar dependendo do valor do ruído. Isso é conveniente porque afetará a opacidade do raio renderizado, assim como na vida real.
Shader de pixel pronto:
cbuffer cbPerFrame : register (b0) { float4 cb0_v0; float4 cb0_v1; float4 cb0_v2; float4 cb0_v3; } cbuffer cbPerFrame : register (b2) { float4 cb2_v0; float4 cb2_v1; float4 cb2_v2; float4 cb2_v3; } cbuffer cbPerFrame : register (b4) { float4 cb4_v0; float4 cb4_v1; float4 cb4_v2; float4 cb4_v3; float4 cb4_v4; } struct VS_OUTPUT { float2 Texcoords : Texcoord0; float4 InstanceLODParams : INSTANCE_LOD_PARAMS; float4 PositionH : SV_Position; };
Resumir
Nesta parte, descrevi uma maneira de renderizar raios em The Witcher 3.
Estou muito satisfeito por o código do assembler que saiu do meu shader corresponder completamente ao original!
Parte 2. Truques do céu bobo
Esta parte será um pouco diferente das anteriores. Nele, quero mostrar alguns aspectos do sombreador do céu Witcher 3.
Por que "truques bobos" e não todo o shader? Bem, há várias razões. Em primeiro lugar, o sombreador de céu Witcher 3 é um animal bastante complexo. O sombreador de pixel da versão 2015 contém 267 linhas de código assembler e o sombreador do DLC Blood and Wine contém 385 linhas.
Além disso, eles recebem muita entrada, o que não é muito propício para a engenharia reversa do código HLSL completo (e legível!).
Portanto, decidi mostrar apenas parte dos truques desses shaders. Se eu encontrar algo novo, complementarei a postagem.
As diferenças entre a versão de 2015 e o DLC (2016) são muito visíveis. Em particular, eles incluem diferenças no cálculo das estrelas e sua cintilação, uma abordagem diferente para renderizar o Sol ...
O shader
Blood and Wine calcula até a Via Láctea à noite.
Vou começar com o básico e depois falar sobre truques estúpidos.
O básico
Como a maioria dos jogos modernos, o Witcher 3 usa o skydome para modelar o céu. Veja o hemisfério usado para isso em Witcher 3 (2015). Nota: neste caso, a caixa delimitadora dessa malha está na faixa de [0,0,0] a [1,1,1] (Z é o eixo apontando para cima) e possui UVs distribuídos suavemente. Depois nós os usamos.
A idéia por trás do skydome é semelhante à idéia do
skybox (a única diferença é a malha usada). No estágio de vértice, transformamos o skydome em relação ao observador (geralmente de acordo com a posição da câmera), o que cria a ilusão de que o céu está realmente muito distante - nunca chegaremos a ele.
Se você leu as partes anteriores desta série de artigos, sabe que o "The Witcher 3" usa a profundidade inversa, ou seja, o plano distante é 0,0f e o plano mais próximo é 1,0f. Para que a saída do skydome seja totalmente executada no plano remoto, nos parâmetros da janela de navegação, definimos
MinDepth com o mesmo valor que
MaxDepth :
Para saber como os
campos MinDepth e
MaxDepth são usados durante a conversão da janela de navegação, clique
aqui (docs.microsoft.com).
Sombreador de vértice
Vamos começar com o vertex shader. Em Witcher 3 (2015), o código do shader do assembler é o seguinte:
vs_5_0 dcl_globalFlags refactoringAllowed dcl_constantbuffer cb1[4], immediateIndexed dcl_constantbuffer cb2[6], immediateIndexed dcl_input v0.xyz dcl_input v1.xy dcl_output o0.xy dcl_output o1.xyz dcl_output_siv o2.xyzw, position dcl_temps 2 0: mov o0.xy, v1.xyxx 1: mad r0.xyz, v0.xyzx, cb2[4].xyzx, cb2[5].xyzx 2: mov r0.w, l(1.000000) 3: dp4 o1.x, r0.xyzw, cb2[0].xyzw 4: dp4 o1.y, r0.xyzw, cb2[1].xyzw 5: dp4 o1.z, r0.xyzw, cb2[2].xyzw 6: mul r1.xyzw, cb1[0].yyyy, cb2[1].xyzw 7: mad r1.xyzw, cb2[0].xyzw, cb1[0].xxxx, r1.xyzw 8: mad r1.xyzw, cb2[2].xyzw, cb1[0].zzzz, r1.xyzw 9: mad r1.xyzw, cb1[0].wwww, l(0.000000, 0.000000, 0.000000, 1.000000), r1.xyzw 10: dp4 o2.x, r0.xyzw, r1.xyzw 11: mul r1.xyzw, cb1[1].yyyy, cb2[1].xyzw 12: mad r1.xyzw, cb2[0].xyzw, cb1[1].xxxx, r1.xyzw 13: mad r1.xyzw, cb2[2].xyzw, cb1[1].zzzz, r1.xyzw 14: mad r1.xyzw, cb1[1].wwww, l(0.000000, 0.000000, 0.000000, 1.000000), r1.xyzw 15: dp4 o2.y, r0.xyzw, r1.xyzw 16: mul r1.xyzw, cb1[2].yyyy, cb2[1].xyzw 17: mad r1.xyzw, cb2[0].xyzw, cb1[2].xxxx, r1.xyzw 18: mad r1.xyzw, cb2[2].xyzw, cb1[2].zzzz, r1.xyzw 19: mad r1.xyzw, cb1[2].wwww, l(0.000000, 0.000000, 0.000000, 1.000000), r1.xyzw 20: dp4 o2.z, r0.xyzw, r1.xyzw 21: mul r1.xyzw, cb1[3].yyyy, cb2[1].xyzw 22: mad r1.xyzw, cb2[0].xyzw, cb1[3].xxxx, r1.xyzw 23: mad r1.xyzw, cb2[2].xyzw, cb1[3].zzzz, r1.xyzw 24: mad r1.xyzw, cb1[3].wwww, l(0.000000, 0.000000, 0.000000, 1.000000), r1.xyzw 25: dp4 o2.w, r0.xyzw, r1.xyzw 26: ret
Nesse caso, o sombreador de vértice transfere apenas cabos de texto e uma posição no espaço mundial para a saída. Em
Blood and Wine, ele também exibe um vetor normal normalizado. Vou considerar a versão de 2015 porque é mais simples.
Veja o buffer constante designado como
cb2 :
Aqui temos uma matriz do mundo (escala uniforme de 100 e transferência em relação à posição da câmera). Nada complicado. cb2_v4 e cb2_v5 são os fatores de escala / desvio usados para converter as posições dos vértices do intervalo [0-1] para o intervalo [-1; 1]. Mas aqui, esses coeficientes "comprimem" o eixo Z (para cima).
Nas partes anteriores da série, tivemos shaders de vértice semelhantes. O algoritmo geral é transferir mais cabos de texto, então
Posição é calculada levando em consideração os coeficientes de escala / desvio, PosiçãoW
é calculada no espaço mundial, então a posição final do espaço de recorte é calculada multiplicando
matWorld e
matViewProj -> seu produto é usado para multiplicar por
Posição para obter a SV_Position final .
Portanto, o HLSL desse vertex shader deve ser algo como isto:
struct InputStruct { float3 param0 : POSITION; float2 param1 : TEXCOORD; float3 param2 : NORMAL; float4 param3 : TANGENT; }; struct OutputStruct { float2 param0 : TEXCOORD0; float3 param1 : TEXCOORD1; float4 param2 : SV_Position; }; OutputStruct EditedShaderVS(in InputStruct IN) { OutputStruct OUT = (OutputStruct)0;
Comparação do meu sombreador (esquerda) e do original (direita):
Uma excelente propriedade do
RenderDoc é que ele permite injetar nosso próprio shader em vez do original, e essas alterações afetarão o pipeline até o final do quadro. Como você pode ver no código HLSL, forneci várias opções para aplicar zoom e transformar a geometria final. Você pode experimentar com eles e obter resultados muito engraçados:
Otimização de vertex shader
Você percebeu o problema do vertex shader original? A multiplicação de vértices de uma matriz por uma matriz é completamente redundante! Encontrei isso em pelo menos alguns shaders de vértice (por exemplo, no shader, uma
cortina de chuva à distância ). Podemos otimizá-lo multiplicando imediatamente o
PositionW pelo
matViewProj !
Portanto, podemos substituir esse código pelo HLSL:
da seguinte maneira:
A versão otimizada nos fornece o seguinte código de montagem:
vs_5_0 dcl_globalFlags refactoringAllowed dcl_constantbuffer CB1[4], immediateIndexed dcl_constantbuffer CB2[6], immediateIndexed dcl_input v0.xyz dcl_input v1.xy dcl_output o0.xy dcl_output o1.xyz dcl_output_siv o2.xyzw, position dcl_temps 2 0: mov o0.xy, v1.xyxx 1: mad r0.xyz, v0.xyzx, cb2[4].xyzx, cb2[5].xyzx 2: mov r0.w, l(1.000000) 3: dp4 r1.x, r0.xyzw, cb2[0].xyzw 4: dp4 r1.y, r0.xyzw, cb2[1].xyzw 5: dp4 r1.z, r0.xyzw, cb2[2].xyzw 6: mov o1.xyz, r1.xyzx 7: mov r1.w, l(1.000000) 8: dp4 o2.x, cb1[0].xyzw, r1.xyzw 9: dp4 o2.y, cb1[1].xyzw, r1.xyzw 10: dp4 o2.z, cb1[2].xyzw, r1.xyzw 11: dp4 o2.w, cb1[3].xyzw, r1.xyzw 12: ret
Como você pode ver, reduzimos o número de instruções de 26 para 12 - uma mudança bastante significativa. Eu não sei o quão difundido esse problema está no jogo, mas, pelo amor de Deus, o CD Projekt Red, talvez lançar um patch? :)
E eu não estou brincando. Você pode inserir meu shader otimizado em vez do RenderDoc original e verá que essa otimização não afeta visualmente nada. Honestamente, eu não entendo por que o CD Projekt Red decidiu realizar a multiplicação de vértices de uma matriz por uma matriz ...
O sol
Em The Witcher 3 (2015), o cálculo da dispersão atmosférica e do Sol consiste em duas chamadas de desenho separadas:
Witcher 3 (2015) - atéWitcher 3 (2015) - com o céuWitcher 3 (2015) - com céu + solA renderização do Sol na versão 2015 é muito semelhante à
renderização da Lua em termos de geometria e estados de mistura / profundidade.
Por outro lado, em
"Sangue e Vinho", o céu com o Sol é representado em uma passagem:
The Witcher 3: Blood and Wine (2016) - Para o CéuThe Witcher 3: Blood and Wine (2016) - com o céu e o solNão importa como você renderize o Sol, em algum momento você ainda precisará da direção (normalizada) da luz solar. A maneira mais lógica de obter esse vetor é usar
coordenadas esféricas . De fato, precisamos apenas de dois valores que indicam dois ângulos (em radianos!):
Phi e
teta . Após recebê-los, podemos assumir que
r = 1 , reduzindo-o. Em seguida, para as coordenadas cartesianas com o eixo Y apontando para cima, você pode escrever o seguinte código em HLSL:
float3 vSunDir; vSunDir.x = sin(fTheta)*cos(fPhi); vSunDir.y = sin(fTheta)*sin(fPhi); vSunDir.z = cos(fTheta); vSunDir = normalize(vSunDir);
Normalmente, a direção da luz solar é calculada no aplicativo e depois passada para o buffer constante para uso futuro.
Tendo recebido a direção da luz solar, podemos nos aprofundar no código de montagem do shader de pixel
"Blood and Wine" ...
... 100: add r1.xyw, -r0.xyxz, cb12[0].xyxz 101: dp3 r2.x, r1.xywx, r1.xywx 102: rsq r2.x, r2.x 103: mul r1.xyw, r1.xyxw, r2.xxxx 104: mov_sat r2.xy, cb12[205].yxyy 105: dp3 r2.z, -r1.xywx, -r1.xywx 106: rsq r2.z, r2.z 107: mul r1.xyw, -r1.xyxw, r2.zzzz ...
Então, primeiro,
cb12 [0] .xyz é a posição da câmera e em
r0.xyz armazenamos a posição do vértice (essa é a saída do shader de vértice). Portanto, a linha 100 calcula o vetor
worldToCamera . Mas dê uma olhada nas linhas 105-107. Podemos escrevê-los como
normalizar (-worldToCamera) , ou seja, calculamos o vetor
cameraToWorld normalizado.
120: dp3_sat r1.x, cb12[203].yzwy, r1.xywx
Em seguida, calculamos o produto escalar dos
vetores cameraToWorld e
sunDirection ! Lembre-se de que eles devem ser normalizados. Também saturamos essa expressão completa para limitá-la ao intervalo [0-1].
Ótimo! Este produto escalar é armazenado em r1.x. Vamos ver onde se aplica a seguir ...
152: log r1.x, r1.x 153: mul r1.x, r1.x, cb12[203].x 154: exp r1.x, r1.x 155: mul r1.x, r2.y, r1.x
A trindade “log, mul, exp” é exponenciação. Como você pode ver, aumentamos nosso cosseno (o produto escalar de vetores normalizados) até certo ponto. Você pode perguntar o porquê. Dessa forma, podemos criar um gradiente que imita o sol. (E a linha 155 afeta a opacidade desse gradiente, de modo que, por exemplo, o redefinimos para ocultar completamente o sol). Aqui estão alguns exemplos:
expoente = 54expoente = 2400Tendo esse gradiente, usamos para interpolar entre
skyColor e
sunColor ! Para evitar artefatos, é necessário saturar o valor na linha 120.
Vale ressaltar que esse truque pode ser usado para simular as
coroas da lua (com valores baixos de expoente). Para fazer isso, precisamos do vetor
moonDirection , que pode ser facilmente calculado usando coordenadas esféricas.
O código HLSL pronto pode ser semelhante ao seguinte trecho:
float3 vCamToWorld = normalize( PosW – CameraPos ); float cosTheta = saturate( dot(vSunDir, vCamToWorld) ); float sunGradient = pow( cosTheta, sunExponent ); float3 color = lerp( skyColor, sunColor, sunGradient );
Movimento das estrelas
Se você fizer um lapso de tempo no céu noturno claro de Witcher 3, poderá ver que as estrelas não são estáticas - elas se movem um pouco pelo céu! Percebi isso quase por acidente e queria saber como foi implementado.
Vamos começar com o fato de que as estrelas em Witcher 3 são apresentadas como um mapa cúbico de tamanho 1024x1024x6. Se você pensar bem, pode entender que esta é uma solução muito conveniente que permite que você tire facilmente as direções para amostrar um mapa cúbico.
Vejamos o seguinte código do assembler:
159: add r1.xyz, -v1.xyzx, cb1[8].xyzx 160: dp3 r0.w, r1.xyzx, r1.xyzx 161: rsq r0.w, r0.w 162: mul r1.xyz, r0.wwww, r1.xyzx 163: mul r2.xyz, cb12[204].zwyz, l(0.000000, 0.000000, 1.000000, 0.000000) 164: mad r2.xyz, cb12[204].yzwy, l(0.000000, 1.000000, 0.000000, 0.000000), -r2.xyzx 165: mul r4.xyz, r2.xyzx, cb12[204].zwyz 166: mad r4.xyz, r2.zxyz, cb12[204].wyzw, -r4.xyzx 167: dp3 r4.x, r1.xyzx, r4.xyzx 168: dp2 r4.y, r1.xyxx, r2.yzyy 169: dp3 r4.z, r1.xyzx, cb12[204].yzwy 170: dp3 r0.w, r4.xyzx, r4.xyzx 171: rsq r0.w, r0.w 172: mul r2.xyz, r0.wwww, r4.xyzx 173: sample_indexable(texturecube)(float,float,float,float) r4.xyz, r2.xyzx, t0.xyzw, s0
Para calcular o vetor de amostragem final (linha 173), começamos computando o vetor
worldToCamera normalizado (linhas 159-162).
Em seguida, calculamos dois produtos vetoriais (163-164, 165-166) com
moonDirection e, posteriormente, calculamos três produtos escalares para obter o vetor de amostragem final. Código HLSL:
float3 vWorldToCamera = normalize( g_CameraPos.xyz - Input.PositionW.xyz ); float3 vMoonDirection = cb12_v204.yzw; float3 vStarsSamplingDir = cross( vMoonDirection, float3(0, 0, 1) ); float3 vStarsSamplingDir2 = cross( vStarsSamplingDir, vMoonDirection ); float dirX = dot( vWorldToCamera, vStarsSamplingDir2 ); float dirY = dot( vWorldToCamera, vStarsSamplingDir ); float dirZ = dot( vWorldToCamera, vMoonDirection); float3 dirXYZ = normalize( float3(dirX, dirY, dirZ) ); float3 starsColor = texNightStars.Sample( samplerAnisoWrap, dirXYZ ).rgb;
Nota para mim mesmo: este é um código muito bem projetado, e devo investigá-lo com mais detalhes.
Nota aos leitores: se você souber mais sobre esta operação, me diga!
Estrelas cintilantes
Outro truque interessante que eu gostaria de explorar com mais detalhes é o tremor das estrelas.
Por exemplo, se você passear por Novigrad em clima claro, notará que as estrelas brilham.Fiquei curioso sobre como isso foi implementado. Verificou-se que a diferença entre a versão de 2015 e "Blood and Wine" é bastante grande. Para simplificar, considerarei a versão de 2015.Então, começamos logo após a amostragem de starsColor da seção anterior: 174: mul r0.w, v0.x, l(100.000000) 175: round_ni r1.w, r0.w 176: mad r2.w, v0.y, l(50.000000), cb0[0].x 177: round_ni r4.w, r2.w 178: bfrev r4.w, r4.w 179: iadd r5.x, r1.w, r4.w 180: ishr r5.y, r5.x, l(13) 181: xor r5.x, r5.x, r5.y 182: imul null, r5.y, r5.x, r5.x 183: imad r5.y, r5.y, l(0x0000ec4d), l(0.0000000000000000000000000000000000001) 184: imad r5.x, r5.x, r5.y, l(146956042240.000000) 185: and r5.x, r5.x, l(0x7fffffff) 186: itof r5.x, r5.x 187: mad r5.y, v0.x, l(100.000000), l(-1.000000) 188: round_ni r5.y, r5.y 189: iadd r4.w, r4.w, r5.y 190: ishr r5.z, r4.w, l(13) 191: xor r4.w, r4.w, r5.z 192: imul null, r5.z, r4.w, r4.w 193: imad r5.z, r5.z, l(0x0000ec4d), l(0.0000000000000000000000000000000000001) 194: imad r4.w, r4.w, r5.z, l(146956042240.000000) 195: and r4.w, r4.w, l(0x7fffffff) 196: itof r4.w, r4.w 197: add r5.z, r2.w, l(-1.000000) 198: round_ni r5.z, r5.z 199: bfrev r5.z, r5.z 200: iadd r1.w, r1.w, r5.z 201: ishr r5.w, r1.w, l(13) 202: xor r1.w, r1.w, r5.w 203: imul null, r5.w, r1.w, r1.w 204: imad r5.w, r5.w, l(0x0000ec4d), l(0.0000000000000000000000000000000000001) 205: imad r1.w, r1.w, r5.w, l(146956042240.000000) 206: and r1.w, r1.w, l(0x7fffffff) 207: itof r1.w, r1.w 208: mul r1.w, r1.w, l(0.000000001) 209: iadd r5.y, r5.z, r5.y 210: ishr r5.z, r5.y, l(13) 211: xor r5.y, r5.y, r5.z 212: imul null, r5.z, r5.y, r5.y 213: imad r5.z, r5.z, l(0x0000ec4d), l(0.0000000000000000000000000000000000001) 214: imad r5.y, r5.y, r5.z, l(146956042240.000000) 215: and r5.y, r5.y, l(0x7fffffff) 216: itof r5.y, r5.y 217: frc r0.w, r0.w 218: add r0.w, -r0.w, l(1.000000) 219: mul r5.z, r0.w, r0.w 220: mul r0.w, r0.w, r5.z 221: mul r5.xz, r5.xxzx, l(0.000000001, 0.000000, 3.000000, 0.000000) 222: mad r0.w, r0.w, l(-2.000000), r5.z 223: frc r2.w, r2.w 224: add r2.w, -r2.w, l(1.000000) 225: mul r5.z, r2.w, r2.w 226: mul r2.w, r2.w, r5.z 227: mul r5.z, r5.z, l(3.000000) 228: mad r2.w, r2.w, l(-2.000000), r5.z 229: mad r4.w, r4.w, l(0.000000001), -r5.x 230: mad r4.w, r0.w, r4.w, r5.x 231: mad r5.x, r5.y, l(0.000000001), -r1.w 232: mad r0.w, r0.w, r5.x, r1.w 233: add r0.w, -r4.w, r0.w 234: mad r0.w, r2.w, r0.w, r4.w 235: mad r2.xyz, r0.wwww, l(0.000500, 0.000500, 0.000500, 0.000000), r2.xyzx 236: sample_indexable(texturecube)(float,float,float,float) r2.xyz, r2.xyzx, t0.xyzw, s0 237: log r4.xyz, r4.xyzx 238: mul r4.xyz, r4.xyzx, l(2.200000, 2.200000, 2.200000, 0.000000) 239: exp r4.xyz, r4.xyzx 240: log r2.xyz, r2.xyzx 241: mul r2.xyz, r2.xyzx, l(2.200000, 2.200000, 2.200000, 0.000000) 242: exp r2.xyz, r2.xyzx 243: mul r2.xyz, r2.xyzx, r4.xyzx
Hum. Vamos dar uma olhada no final deste código de montagem bastante longo.Após a amostragem de starsColor na linha 173, calculamos algum tipo de valor de deslocamento . Esse deslocamento é usado para distorcer a primeira direção da amostragem (r2.xyz, linha 235) e, novamente, amostramos o mapa cúbico de estrelas, executamos a correção gama desses dois valores (237-242) e os multiplicamos (243).Simples, certo? Bem, na verdade não. Vamos pensar um pouco sobre esse deslocamento . Esse valor deve ser diferente em todo o skydome - estrelas igualmente cintilantes pareceriam muito irrealistas.Para compensarfor o mais diversificado possível, tiraremos proveito do fato de que os UVs são estendidos para skydome (v0.xy) e aplicaremos o tempo decorrido armazenado no buffer constante (cb [0] .x).Se você não estiver familiarizado com esses assustadores ishr / xor / e, na parte sobre o efeito do raio, leia sobre o ruído inteiro.Como você pode ver, o ruído inteiro é causado quatro vezes aqui, mas difere do usado para raios. Para tornar os resultados ainda mais aleatórios, o número inteiro de entrada para ruído é a soma ( iadd ) e os bits são invertidos com ele (função interna reversebits ; instrução bfrev ).Então, agora diminua a velocidade. Vamos começar do começo.Temos 4 "iterações" de ruído inteiro. Analisei o código do assembler, os cálculos de todas as 4 iterações são assim: int getInt( float x ) { return asint( floor(x) ); } int getReverseInt( float x ) { return reversebits( getInt(x) ); }
A saída final de todas as 4 iterações (para localizá-las, siga as instruções de itof ):Iteração 1 - r5.x,Iteração 2 - r4.w,Iteração 3 - r1.w,Iteração 4 - r5.yApós o último itof (linha 216 ) temos: 217: frc r0.w, r0.w 218: add r0.w, -r0.w, l(1.000000) 219: mul r5.z, r0.w, r0.w 220: mul r0.w, r0.w, r5.z 221: mul r5.xz, r5.xxzx, l(0.000000001, 0.000000, 3.000000, 0.000000) 222: mad r0.w, r0.w, l(-2.000000), r5.z 223: frc r2.w, r2.w 224: add r2.w, -r2.w, l(1.000000) 225: mul r5.z, r2.w, r2.w 226: mul r2.w, r2.w, r5.z 227: mul r5.z, r5.z, l(3.000000) 228: mad r2.w, r2.w, l(-2.000000), r5.z
Essas linhas calculam os valores da curva S para a balança com base na parte fracionária do UV, como no caso de raios. Então:
float s_curve( float x ) { float x2 = x * x; float x3 = x2 * x;
Como você pode esperar, esses coeficientes são usados para interpolar suavemente o ruído e gerar o deslocamento final para as coordenadas de amostragem: 229: mad r4.w, r4.w, l(0.000000001), -r5.x 230: mad r4.w, r0.w, r4.w, r5.x float noise0 = lerp( fStarsNoise1, fStarsNoise2, weightX ); 231: mad r5.x, r5.y, l(0.000000001), -r1.w 232: mad r0.w, r0.w, r5.x, r1.w float noise1 = lerp( fStarsNoise3, fStarsNoise4, weightX ); 233: add r0.w, -r4.w, r0.w 234: mad r0.w, r2.w, r0.w, r4.w float offset = lerp( noise0, noise1, weightY ); 235: mad r2.xyz, r0.wwww, l(0.000500, 0.000500, 0.000500, 0.000000), r2.xyzx 236: sample_indexable(texturecube)(float,float,float,float) r2.xyz, r2.xyzx, t0.xyzw, s0 float3 starsPerturbedDir = dirXYZ + offset * 0.0005; float3 starsColorDisturbed = texNightStars.Sample( samplerAnisoWrap, starsPerturbedDir ).rgb;
Aqui está uma pequena visualização do deslocamento calculado :Após o cálculo de starsColorDisturbed, a parte mais difícil está concluída. Viva!
A próxima etapa é executar a correção gama para starsColor e starsColorDisturbed , após o que são multiplicados: starsColor = pow( starsColor, 2.2 ); starsColorDisturbed = pow( starsColorDisturbed, 2.2 ); float3 starsFinal = starsColor * starsColorDisturbed;
Estrelas - os retoques finais
Temos starsFinal em r1.xyz. No final do processamento em estrela, ocorre o seguinte: 256: log r1.xyz, r1.xyzx 257: mul r1.xyz, r1.xyzx, l(2.500000, 2.500000, 2.500000, 0.000000) 258: exp r1.xyz, r1.xyzx 259: min r1.xyz, r1.xyzx, l(1.000000, 1.000000, 1.000000, 0.000000) 260: add r0.w, -cb0[9].w, l(1.000000) 261: mul r1.xyz, r0.wwww, r1.xyzx 262: mul r1.xyz, r1.xyzx, l(10.000000, 10.000000, 10.000000, 0.000000)
Isso é muito mais fácil comparado às estrelas cintilantes e em movimento.Então, começamos elevando starsFinal a uma potência de 2,5 - isso nos permite controlar a densidade das estrelas. Muito esperto. Então tornamos a cor máxima das estrelas igual a float3 (1, 1, 1).cb0 [9] .w é usado para controlar a visibilidade geral das estrelas. Portanto, podemos esperar que durante o dia esse valor seja 1,0 (o que dá uma multiplicação por zero) e à noite - 0,0.No final, aumentamos a visibilidade das estrelas em 10. E é isso!Parte 3. O Dom Witcher (objetos e mapa de brilho)
Quase todos os efeitos e técnicas descritos anteriormente não estavam realmente associados ao Witcher 3. Coisas como correção de tom, vinhetas ou cálculo do brilho médio estão presentes em quase todos os jogos modernos. Até o efeito da intoxicação é bastante difundido.Por isso, decidi examinar mais de perto a mecânica de renderização do "instinto bruxo". Geralt é um bruxo e, portanto, seus sentimentos são muito mais aguçados do que os de uma pessoa comum. Conseqüentemente, ele pode ver e ouvir mais do que outras pessoas, o que o ajuda muito em suas investigações. A mecânica do talento do bruxo permite ao jogador visualizar esses traços.Aqui está uma demonstração do efeito:E mais um, com melhor iluminação:Como você pode ver, existem dois tipos de objetos: aqueles com os quais Geralt pode interagir (contorno amarelo) e traços associados à investigação (contorno vermelho). Depois de Geralt examinar a trilha vermelha, ela pode se transformar em amarelo (primeiro vídeo). Observe que a tela inteira fica cinza e um efeito de olho de peixe (segundo vídeo) é adicionado.Esse efeito é bastante complicado, então decidi dividir sua pesquisa em três partes.No primeiro, falarei sobre a seleção de objetos, no segundo - sobre a geração do contorno e no terceiro - sobre a unificação final de tudo isso em um todo.Selecionar objetos
Como eu disse, existem dois tipos de objetos e precisamos distinguir entre eles. No Witcher 3, isso é implementado usando um buffer de estêncil. Ao gerar malhas GBuffer que devem ser marcadas como “traços” (vermelho), elas são renderizadas com estêncil = 8. Malhas marcadas em amarelo como objetos “interessantes” são renderizadas com estêncil = 4.Por exemplo, as duas texturas a seguir mostram um exemplo de quadro com instinto witcher visível e o buffer de estêncil correspondente:Resumo do buffer do estêncil
O buffer de estêncil é frequentemente usado em jogos para marcar malhas. Certas categorias de malhas recebem o mesmo ID.A idéia é usar a função Sempre com o operador Substituir se o teste de estêncil for bem-sucedido e com o operador Manter em todos os outros casos.Veja como é implementado usando o D3D11: D3D11_DEPTH_STENCIL_DESC depthstencilState;
O valor stensil a ser gravado no buffer é passado como um StencilRef na chamada da API:
Brilho de renderização
Nesta passagem, do ponto de vista da implementação, existe uma textura de tela cheia no formato R11G11B10_FLOAT, na qual objetos e traços interessantes são armazenados nos canais R e G.Por que precisamos disso em termos de brilho? Acontece que o instinto de Geralt tem um raio limitado, então os objetos só ficam contornos quando o jogador está perto o suficiente deles.Veja este aspecto em ação:Começamos limpando a textura do brilho, preenchendo-a com preto.Em seguida, são feitas duas chamadas de desenho em tela cheia: a primeira para o rastreamento, a segunda para objetos interessantes:A primeira chamada de empate é feita para traços - o canal verde:A segunda chamada é feita para objetos interessantes - o canal vermelho:Bem, mas como determinamos quais pixels considerar? Teremos que usar o buffer de estêncil!Para cada uma dessas chamadas, é realizado um teste de estêncil e somente os pixels que foram marcados anteriormente como “8” (primeira chamada de desenho) ou “4” são aceitos.Visualização do teste de estêncil para traços:... e para objetos interessantes:Como o teste é realizado neste caso? Você pode aprender sobre o básico do teste de estêncil em uma boa publicação . Em geral, a fórmula de teste do estêncil tem a seguinte forma: if (StencilRef & StencilReadMask OP StencilValue & StencilReadMask) accept pixel else discard pixel
em que:StencilRef é o valor passado pela chamada da API,StencilReadMask é a máscara usada para ler o valor stensil (observe que ele está presente nos lados esquerdo e direito),OP é o operador de comparação, definido via API,StencilValue é o valor do buffer de estêncil no pixel atual sendo processado.É importante entender que usamos ANDs binários para calcular os operandos.Depois de se familiarizar com o básico, vamos ver como esses parâmetros são usados nessas chamadas de empate:Condição de estêncil para traçosEstado de estêncil para objetos interessantesHa! Como podemos ver, a única diferença é o ReadMask. Vamos conferir! Substitua estes valores na equação de teste do estêncil: Let StencilReadMask = 0x08 and StencilRef = 0: For a pixel with stencil = 8: 0 & 0x08 < 8 & 0x08 0 < 8 TRUE For a pixel with stencil = 4: 0 & 0x08 < 4 & 0x08 0 < 0 FALSE
Inteligentemente. Como você pode ver, neste caso, não comparamos o valor stensil, mas verificamos se um determinado bit do buffer de estêncil está definido. Cada pixel do buffer de estêncil possui o formato uint8, portanto, o intervalo de valores é [0-255].Nota: todas as chamadas DrawIndexed (36) estão relacionadas à renderização de pegadas como rastreios; portanto, nesse quadro específico, o mapa de brilho tem a seguinte forma final:Mas antes do teste de estêncil, existe um sombreador de pixels. 28738 e 28748 usam o mesmo sombreador de pixels: ps_5_0 dcl_globalFlags refactoringAllowed dcl_constantbuffer cb0[2], immediateIndexed dcl_constantbuffer cb3[8], immediateIndexed dcl_constantbuffer cb12[214], immediateIndexed dcl_sampler s15, mode_default dcl_resource_texture2d (float,float,float,float) t15 dcl_input_ps_siv v0.xy, position dcl_output o0.xyzw dcl_output o1.xyzw dcl_output o2.xyzw dcl_output o3.xyzw dcl_temps 2 0: mul r0.xy, v0.xyxx, cb0[1].zwzz 1: sample_indexable(texture2d)(float,float,float,float) r0.x, r0.xyxx, t15.xyzw, s15 2: mul r1.xyzw, v0.yyyy, cb12[211].xyzw 3: mad r1.xyzw, cb12[210].xyzw, v0.xxxx, r1.xyzw 4: mad r0.xyzw, cb12[212].xyzw, r0.xxxx, r1.xyzw 5: add r0.xyzw, r0.xyzw, cb12[213].xyzw 6: div r0.xyz, r0.xyzx, r0.wwww 7: add r0.xyz, r0.xyzx, -cb3[7].xyzx 8: dp3 r0.x, r0.xyzx, r0.xyzx 9: sqrt r0.x, r0.x 10: mul r0.y, r0.x, l(0.120000) 11: log r1.x, abs(cb3[6].y) 12: mul r1.xy, r1.xxxx, l(2.800000, 0.800000, 0.000000, 0.000000) 13: exp r1.xy, r1.xyxx 14: mad r0.zw, r1.xxxy, l(0.000000, 0.000000, 120.000000, 120.000000), l(0.000000, 0.000000, 1.000000, 1.000000) 15: lt r1.x, l(0.030000), cb3[6].y 16: movc r0.xy, r1.xxxx, r0.yzyy, r0.xwxx 17: div r0.x, r0.x, r0.y 18: log r0.x, r0.x 19: mul r0.x, r0.x, l(1.600000) 20: exp r0.x, r0.x 21: add r0.x, -r0.x, l(1.000000) 22: max r0.x, r0.x, l(0) 23: mul o0.xyz, r0.xxxx, cb3[0].xyzx 24: mov o0.w, cb3[0].w 25: mov o1.xyzw, cb3[1].xyzw 26: mov o2.xyzw, cb3[2].xyzw 27: mov o3.xyzw, cb3[3].xyzw 28: ret
Esse sombreador de pixels grava em apenas um destino de renderização, portanto as linhas 24 a 27 são redundantes.A primeira coisa que acontece aqui é a amostragem de profundidade (com um amostrador de pontos com um limite de valor), linha 1. Esse valor é usado para recriar uma posição no mundo multiplicando por uma matriz especial, seguida pela divisão de perspectiva (linhas 2-6).Tomando a posição de Geralt (cb3 [7] .xyz - observe que essa não é a posição da câmera!), Calculamos a distância de Geralt a esse ponto específico (linhas 7-9).A seguinte entrada é importante neste shader:- cb3 [0] .rgb - cor de saída. Pode ter o formato float3 (0, 1, 0) (traços) ou float3 (1, 0, 0) (objetos interessantes),- cb3 [6] .y - fator de escala de distância. Afeta diretamente o raio e o brilho da saída final.Mais tarde, temos fórmulas bastante complicadas para calcular o brilho, dependendo da distância entre Geralt e o objeto. Eu posso assumir que todos os coeficientes são selecionados experimentalmente.A saída final é de cor * intensidade .O código HLSL será mais ou menos assim: struct FSInput { float4 param0 : SV_Position; }; struct FSOutput { float4 param0 : SV_Target0; float4 param1 : SV_Target1; float4 param2 : SV_Target2; float4 param3 : SV_Target3; }; float3 getWorldPos( float2 screenPos, float depth ) { float4 worldPos = float4(screenPos, depth, 1.0); worldPos = mul( worldPos, screenToWorld ); return worldPos.xyz / worldPos.w; } FSOutput EditedShaderPS(in FSInput IN) {
Uma pequena comparação do código de sombreador do assembler original (esquerda) e (direita).Este foi o primeiro estágio do efeito de talento da bruxa . De fato, é o mais simples.Parte 4. O Dom Witcher (mapa de descrição)
Mais uma vez, dê uma olhada na cena que estamos explorando:Na primeira parte da análise do efeito do instinto da bruxa, mostrei como o "mapa de brilho" é gerado.Temos uma textura de tela cheia do formato R11G11B10_FLOAT, que pode ser assim:O canal verde significa "pegadas", o vermelho - objetos interessantes com os quais Geralt pode interagir.Tendo recebido essa textura, podemos avançar para a próxima etapa - chamei de “mapa de contorno”.Essa é uma textura um pouco estranha do formato 512x512 R16G16_FLOAT. É importante que seja implementado no estilo de "pingue-pongue". O mapa de contorno do quadro anterior são os dados de entrada (junto com o mapa de brilho) para gerar um novo mapa de contorno no quadro atual.Os buffers de ping-pong podem ser implementados de várias maneiras, mas eu pessoalmente gosto do seguinte (pseudo-código) acima de tudo:
Essa abordagem, onde a entrada é sempre [m_outlineIndex] e a saída é sempre [! M_outlineIndex] , fornece flexibilidade para o uso de pós-efeitos adicionais.Vamos dar uma olhada no pixel shader: ps_5_0 dcl_globalFlags refactoringAllowed dcl_constantbuffer cb3[1], immediateIndexed dcl_sampler s0, mode_default dcl_sampler s1, mode_default dcl_resource_texture2d (float,float,float,float) t0 dcl_resource_texture2d (float,float,float,float) t1 dcl_input_ps linear v2.xy dcl_output o0.xyzw dcl_temps 4 0: add r0.xyzw, v2.xyxy, v2.xyxy 1: round_ni r1.xy, r0.zwzz 2: frc r0.xyzw, r0.xyzw 3: add r1.zw, r1.xxxy, l(0.000000, 0.000000, -1.000000, -1.000000) 4: dp2 r1.z, r1.zwzz, r1.zwzz 5: add r1.z, -r1.z, l(1.000000) 6: max r2.w, r1.z, l(0) 7: dp2 r1.z, r1.xyxx, r1.xyxx 8: add r3.xyzw, r1.xyxy, l(-1.000000, -0.000000, -0.000000, -1.000000) 9: add r1.x, -r1.z, l(1.000000) 10: max r2.x, r1.x, l(0) 11: dp2 r1.x, r3.xyxx, r3.xyxx 12: dp2 r1.y, r3.zwzz, r3.zwzz 13: add r1.xy, -r1.xyxx, l(1.000000, 1.000000, 0.000000, 0.000000) 14: max r2.yz, r1.xxyx, l(0, 0, 0, 0) 15: sample_indexable(texture2d)(float,float,float,float) r1.xyzw, r0.zwzz, t1.xyzw, s1 16: dp4 r1.x, r1.xyzw, r2.xyzw 17: add r2.xyzw, r0.zwzw, l(0.003906, 0.000000, -0.003906, 0.000000) 18: add r0.xyzw, r0.xyzw, l(0.000000, 0.003906, 0.000000, -0.003906) 19: sample_indexable(texture2d)(float,float,float,float) r1.yz, r2.xyxx, t1.zxyw, s1 20: sample_indexable(texture2d)(float,float,float,float) r2.xy, r2.zwzz, t1.xyzw, s1 21: add r1.yz, r1.yyzy, -r2.xxyx 22: sample_indexable(texture2d)(float,float,float,float) r0.xy, r0.xyxx, t1.xyzw, s1 23: sample_indexable(texture2d)(float,float,float,float) r0.zw, r0.zwzz, t1.zwxy, s1 24: add r0.xy, -r0.zwzz, r0.xyxx 25: max r0.xy, abs(r0.xyxx), abs(r1.yzyy) 26: min r0.xy, r0.xyxx, l(1.000000, 1.000000, 0.000000, 0.000000) 27: mul r0.xy, r0.xyxx, r1.xxxx 28: sample_indexable(texture2d)(float,float,float,float) r0.zw, v2.xyxx, t0.zwxy, s0 29: mad r0.w, r1.x, l(0.150000), r0.w 30: mad r0.x, r0.x, l(0.350000), r0.w 31: mad r0.x, r0.y, l(0.350000), r0.x 32: mul r0.yw, cb3[0].zzzw, l(0.000000, 300.000000, 0.000000, 300.000000) 33: mad r0.yw, v2.xxxy, l(0.000000, 150.000000, 0.000000, 150.000000), r0.yyyw 34: ftoi r0.yw, r0.yyyw 35: bfrev r0.w, r0.w 36: iadd r0.y, r0.w, r0.y 37: ishr r0.w, r0.y, l(13) 38: xor r0.y, r0.y, r0.w 39: imul null, r0.w, r0.y, r0.y 40: imad r0.w, r0.w, l(0x0000ec4d), l(0.0000000000000000000000000000000000001) 41: imad r0.y, r0.y, r0.w, l(146956042240.000000) 42: and r0.y, r0.y, l(0x7fffffff) 43: itof r0.y, r0.y 44: mad r0.y, r0.y, l(0.000000001), l(0.650000) 45: add_sat r1.xyzw, v2.xyxy, l(0.001953, 0.000000, -0.001953, 0.000000) 46: sample_indexable(texture2d)(float,float,float,float) r0.w, r1.xyxx, t0.yzwx, s0 47: sample_indexable(texture2d)(float,float,float,float) r1.x, r1.zwzz, t0.xyzw, s0 48: add r0.w, r0.w, r1.x 49: add_sat r1.xyzw, v2.xyxy, l(0.000000, 0.001953, 0.000000, -0.001953) 50: sample_indexable(texture2d)(float,float,float,float) r1.x, r1.xyxx, t0.xyzw, s0 51: sample_indexable(texture2d)(float,float,float,float) r1.y, r1.zwzz, t0.yxzw, s0 52: add r0.w, r0.w, r1.x 53: add r0.w, r1.y, r0.w 54: mad r0.w, r0.w, l(0.250000), -r0.z 55: mul r0.w, r0.y, r0.w 56: mul r0.y, r0.y, r0.z 57: mad r0.x, r0.w, l(0.900000), r0.x 58: mad r0.y, r0.y, l(-0.240000), r0.x 59: add r0.x, r0.y, r0.z 60: mov_sat r0.z, cb3[0].x 61: log r0.z, r0.z 62: mul r0.z, r0.z, l(100.000000) 63: exp r0.z, r0.z 64: mad r0.z, r0.z, l(0.160000), l(0.700000) 65: mul o0.xy, r0.zzzz, r0.xyxx 66: mov o0.zw, l(0, 0, 0, 0) 67: ret
Como você pode ver, o mapa de contorno de saída é dividido em quatro quadrados iguais, e esta é a primeira coisa que precisamos estudar: 0: add r0.xyzw, v2.xyxy, v2.xyxy 1: round_ni r1.xy, r0.zwzz 2: frc r0.xyzw, r0.xyzw 3: add r1.zw, r1.xxxy, l(0.000000, 0.000000, -1.000000, -1.000000) 4: dp2 r1.z, r1.zwzz, r1.zwzz 5: add r1.z, -r1.z, l(1.000000) 6: max r2.w, r1.z, l(0) 7: dp2 r1.z, r1.xyxx, r1.xyxx 8: add r3.xyzw, r1.xyxy, l(-1.000000, -0.000000, -0.000000, -1.000000) 9: add r1.x, -r1.z, l(1.000000) 10: max r2.x, r1.x, l(0) 11: dp2 r1.x, r3.xyxx, r3.xyxx 12: dp2 r1.y, r3.zwzz, r3.zwzz 13: add r1.xy, -r1.xyxx, l(1.000000, 1.000000, 0.000000, 0.000000) 14: max r2.yz, r1.xxyx, l(0, 0, 0, 0)
Começamos calculando o piso (TextureUV * 2.0), o que nos fornece o seguinte:Para determinar os quadrados individuais, uma pequena função é usada: float getParams(float2 uv) { float d = dot(uv, uv); d = 1.0 - d; d = max( d, 0.0 ); return d; }
Observe que a função retorna 1,0 com a entrada float2 (0,0, 0,0).Este caso ocorre no canto superior esquerdo. Para obter a mesma situação no canto superior direito, subtraia float2 (1, 0) dos cabos de texto arredondados, subtraia float2 (0, 1) para o quadrado verde e float2 (1,0, 1,0) para o quadrado amarelo.Então:
float2 flooredTextureUV = floor( 2.0 * TextureUV ); ... float2 uv1 = flooredTextureUV; float2 uv2 = flooredTextureUV + float2(-1.0, -0.0); float2 uv3 = flooredTextureUV + float2( -0.0, -1.0); float2 uv4 = flooredTextureUV + float2(-1.0, -1.0); float4 mask; mask.x = getParams( uv1 ); mask.y = getParams( uv2 ); mask.z = getParams( uv3 ); mask.w = getParams( uv4 );
Cada um dos componentes da máscara é zero ou um e é responsável por um quadrado da textura. Por exemplo, mask.r e mask.w :mask.rmask.wTemos máscara , vamos seguir em frente. A linha 15 mostra o mapa de luminância. Observe que a textura do brilho está no formato R11G11B10_FLOAT, embora amostremos todos os componentes rgba. Nessa situação, supõe-se que .a seja 1.0f.Os Texcoords usados para esta operação podem ser calculados como frac (TextureUV * 2.0) . Portanto, o resultado dessa operação pode, por exemplo, ter a seguinte aparência:Veja a semelhança?A próxima etapa é muito inteligente - o produto escalar de quatro componentes (dp4) é executado: 16: dp4 r1.x, r1.xyzw, r2.xyzw
Portanto, apenas o canal vermelho (ou seja, apenas objetos interessantes) permanece no canto superior esquerdo, apenas o canal verde no canto superior direito (apenas traços) e tudo no canto inferior direito (porque o componente de luminância .w está indiretamente definido como 1,0). Ótima ideia. O resultado do produto escalar se parece com o seguinte:Após receber este masterFilter , estamos prontos para determinar os contornos dos objetos. Não é tão difícil quanto pode parecer. O algoritmo é muito semelhante ao usado para obter nitidez - precisamos obter a diferença absoluta máxima de valores.Eis o que acontece: amostramos quatro texels ao lado do texel atual que está sendo processado (importante: nesse caso, o tamanho do texel é 1,0 / 256,0!) E calculamos as diferenças absolutas máximas para os canais vermelho e verde: float fTexel = 1.0 / 256; float2 sampling1 = TextureUV + float2( fTexel, 0 ); float2 sampling2 = TextureUV + float2( -fTexel, 0 ); float2 sampling3 = TextureUV + float2( 0, fTexel ); float2 sampling4 = TextureUV + float2( 0, -fTexel ); float2 intensity_x0 = texIntensityMap.Sample( sampler1, sampling1 ).xy; float2 intensity_x1 = texIntensityMap.Sample( sampler1, sampling2 ).xy; float2 intensity_diff_x = intensity_x0 - intensity_x1; float2 intensity_y0 = texIntensityMap.Sample( sampler1, sampling3 ).xy; float2 intensity_y1 = texIntensityMap.Sample( sampler1, sampling4 ).xy; float2 intensity_diff_y = intensity_y0 - intensity_y1; float2 maxAbsDifference = max( abs(intensity_diff_x), abs(intensity_diff_y) ); maxAbsDifference = saturate(maxAbsDifference);
Agora, se multiplicarmos o filtro por maxAbsDifference ...Muito simples e eficiente.Após receber os contornos, fazemos uma amostra do mapa de contornos do quadro anterior.Então, para obter um efeito "fantasmagórico", tomamos parte dos parâmetros calculados na passagem atual e dos valores do mapa de contorno.Diga olá para o nosso velho amigo - ruído inteiro. Ele está presente aqui. Os parâmetros de animação (cb3 [0] .zw) são obtidos do buffer constante e mudam com o tempo. float2 outlines = masterFilter * maxAbsDifference;
Nota: se você quiser implementar o instinto da bruxa, recomendo limitar o ruído inteiro ao intervalo [-1; 1] (como dito em seu site). Não havia nenhuma restrição no shader TW3 original, mas sem ele obtive artefatos terríveis e todo o mapa do esboço era instável.Depois, amostramos o mapa de contorno da mesma maneira que o mapa de brilho anteriormente (desta vez, o texel tem um tamanho de 1.0 / 512.0) e calculamos o valor médio do componente .x:
Então, a julgar pelo código do montador, é calculada a diferença entre a média e o valor desse pixel específico, após o qual a distorção por ruído inteiro é realizada:
O próximo passo é distorcer o valor do mapa de contorno “antigo” usando ruído - esta é a linha principal que confere à textura de saída uma sensação de bloco.Depois, há outros cálculos, após os quais, no final, a “atenuação” é calculada.
Aqui está um pequeno vídeo demonstrando um mapa de estrutura de tópicos em ação:Se você está interessado no sombreador de pixels completo, ele está disponível aqui . Shader é compatível com o RenderDoc.É interessante (e, para ser honesto, um pouco chato) que, apesar da identidade do código do montador com o shader original de Witcher 3, a aparência final do mapa de contorno no RenderDoc esteja mudando!Nota: na última passagem (veja a próxima parte), você verá que apenas o canal .r do mapa de contorno é usado. Por que então precisamos do canal .g? Eu acho que isso é algum tipo de buffer de ping-pong em uma textura - observe que .r contém o canal .g + algum novo valor.Parte 5: O Dom Witcher (Olho de Peixe e o resultado final)
Listaremos brevemente o que já temos: na primeira parte, dedicada ao instinto do bruxo, é gerado um mapa de brilho em tela cheia que mostra o quão perceptível o efeito deve ser dependendo da distância. Na segunda parte, explorei o mapa de contorno com mais detalhes, responsável pelos contornos e pela animação do efeito final.Chegamos ao último estágio. Tudo isso precisa ser combinado! O último passe é um quad em tela cheia. Entradas: buffer de cores, mapa de contorno e mapa de luminância.Para:Depois:
Mais uma vez vou mostrar o vídeo com o efeito aplicado:Como você pode ver, além de aplicar contornos a objetos que Geralt pode ver ou ouvir, o efeito olho de peixe é aplicado a toda a tela, e toda a tela (especialmente os cantos) fica acinzentada para transmitir a sensação de um verdadeiro caçador de monstros.Código completo do shader de pixel montado: ps_5_0 dcl_globalFlags refactoringAllowed dcl_constantbuffer cb0[3], immediateIndexed dcl_constantbuffer cb3[7], immediateIndexed dcl_sampler s0, mode_default dcl_sampler s2, mode_default dcl_resource_texture2d (float,float,float,float) t0 dcl_resource_texture2d (float,float,float,float) t2 dcl_resource_texture2d (float,float,float,float) t3 dcl_input_ps_siv v0.xy, position dcl_output o0.xyzw dcl_temps 7 0: div r0.xy, v0.xyxx, cb0[2].xyxx 1: mad r0.zw, r0.xxxy, l(0.000000, 0.000000, 2.000000, 2.000000), l(0.000000, 0.000000, -1.000000, -1.000000) 2: mov r1.yz, abs(r0.zzwz) 3: div r0.z, cb0[2].x, cb0[2].y 4: mul r1.x, r0.z, r1.y 5: add r0.zw, r1.xxxz, -cb3[2].xxxy 6: mul_sat r0.zw, r0.zzzw, l(0.000000, 0.000000, 0.555556, 0.555556) 7: log r0.zw, r0.zzzw 8: mul r0.zw, r0.zzzw, l(0.000000, 0.000000, 2.500000, 2.500000) 9: exp r0.zw, r0.zzzw 10: dp2 r0.z, r0.zwzz, r0.zwzz 11: sqrt r0.z, r0.z 12: min r0.z, r0.z, l(1.000000) 13: add r0.z, -r0.z, l(1.000000) 14: mov_sat r0.w, cb3[6].x 15: add_sat r1.xy, -r0.xyxx, l(0.030000, 0.030000, 0.000000, 0.000000) 16: add r1.x, r1.y, r1.x 17: add_sat r0.xy, r0.xyxx, l(-0.970000, -0.970000, 0.000000, 0.000000) 18: add r0.x, r0.x, r1.x 19: add r0.x, r0.y, r0.x 20: mul r0.x, r0.x, l(20.000000) 21: min r0.x, r0.x, l(1.000000) 22: add r1.xy, v0.xyxx, v0.xyxx 23: div r1.xy, r1.xyxx, cb0[2].xyxx 24: add r1.xy, r1.xyxx, l(-1.000000, -1.000000, 0.000000, 0.000000) 25: dp2 r0.y, r1.xyxx, r1.xyxx 26: mul r1.xy, r0.yyyy, r1.xyxx 27: mul r0.y, r0.w, l(0.100000) 28: mul r1.xy, r0.yyyy, r1.xyxx 29: max r1.xy, r1.xyxx, l(-0.400000, -0.400000, 0.000000, 0.000000) 30: min r1.xy, r1.xyxx, l(0.400000, 0.400000, 0.000000, 0.000000) 31: mul r1.xy, r1.xyxx, cb3[1].xxxx 32: mul r1.zw, r1.xxxy, cb0[2].zzzw 33: mad r1.zw, v0.xxxy, cb0[1].zzzw, -r1.zzzw 34: sample_indexable(texture2d)(float,float,float,float) r2.xyz, r1.zwzz, t0.xyzw, s0 35: mul r3.xy, r1.zwzz, l(0.500000, 0.500000, 0.000000, 0.000000) 36: sample_indexable(texture2d)(float,float,float,float) r0.y, r3.xyxx, t2.yxzw, s2 37: mad r3.xy, r1.zwzz, l(0.500000, 0.500000, 0.000000, 0.000000), l(0.500000, 0.000000, 0.000000, 0.000000) 38: sample_indexable(texture2d)(float,float,float,float) r2.w, r3.xyxx, t2.yzwx, s2 39: mul r2.w, r2.w, l(0.125000) 40: mul r3.x, cb0[0].x, l(0.100000) 41: add r0.x, -r0.x, l(1.000000) 42: mul r0.xy, r0.xyxx, l(0.030000, 0.125000, 0.000000, 0.000000) 43: mov r3.yzw, l(0, 0, 0, 0) 44: mov r4.x, r0.y 45: mov r4.y, r2.w 46: mov r4.z, l(0) 47: loop 48: ige r4.w, r4.z, l(8) 49: breakc_nz r4.w 50: itof r4.w, r4.z 51: mad r4.w, r4.w, l(0.785375), -r3.x 52: sincos r5.x, r6.x, r4.w 53: mov r6.y, r5.x 54: mul r5.xy, r0.xxxx, r6.xyxx 55: mad r5.zw, r5.xxxy, l(0.000000, 0.000000, 0.125000, 0.125000), r1.zzzw 56: mul r6.xy, r5.zwzz, l(0.500000, 0.500000, 0.000000, 0.000000) 57: sample_indexable(texture2d)(float,float,float,float) r4.w, r6.xyxx, t2.yzwx, s2 58: mad r4.x, r4.w, l(0.125000), r4.x 59: mad r5.zw, r5.zzzw, l(0.000000, 0.000000, 0.500000, 0.500000), l(0.000000, 0.000000, 0.500000, 0.000000) 60: sample_indexable(texture2d)(float,float,float,float) r4.w, r5.zwzz, t2.yzwx, s2 61: mad r4.y, r4.w, l(0.125000), r4.y 62: mad r5.xy, r5.xyxx, r1.xyxx, r1.zwzz 63: sample_indexable(texture2d)(float,float,float,float) r5.xyz, r5.xyxx, t0.xyzw, s0 64: mad r3.yzw, r5.xxyz, l(0.000000, 0.125000, 0.125000, 0.125000), r3.yyzw 65: iadd r4.z, r4.z, l(1) 66: endloop 67: sample_indexable(texture2d)(float,float,float,float) r0.xy, r1.zwzz, t3.xyzw, s0 68: mad_sat r0.xy, -r0.xyxx, l(0.800000, 0.750000, 0.000000, 0.000000), r4.xyxx 69: dp3 r1.x, r3.yzwy, l(0.300000, 0.300000, 0.300000, 0.000000) 70: add r1.yzw, -r1.xxxx, r3.yyzw 71: mad r1.xyz, r0.zzzz, r1.yzwy, r1.xxxx 72: mad r1.xyz, r1.xyzx, l(0.600000, 0.600000, 0.600000, 0.000000), -r2.xyzx 73: mad r1.xyz, r0.wwww, r1.xyzx, r2.xyzx 74: mul r0.yzw, r0.yyyy, cb3[4].xxyz 75: mul r2.xyz, r0.xxxx, cb3[5].xyzx 76: mad r0.xyz, r0.yzwy, l(1.200000, 1.200000, 1.200000, 0.000000), r2.xyzx 77: mov_sat r2.xyz, r0.xyzx 78: dp3_sat r0.x, r0.xyzx, l(1.000000, 1.000000, 1.000000, 0.000000) 79: add r0.yzw, -r1.xxyz, r2.xxyz 80: mad o0.xyz, r0.xxxx, r0.yzwy, r1.xyzx 81: mov o0.w, l(1.000000) 82: ret
82 linhas - por isso temos muito trabalho a fazer!Primeiro, dê uma olhada nos dados de entrada:
O principal valor responsável pela magnitude do efeito é fisheyeAmount . Eu acho que gradualmente aumenta de 0,0 para 1,0 quando Geralt começa a usar seu instinto. O restante dos valores dificilmente muda, mas suspeito que alguns deles seriam diferentes se o usuário tivesse desativado o efeito olho de peixe nas opções (não verifiquei isso).A primeira coisa que acontece aqui é que o shader calcula a máscara responsável pelos ângulos cinzas: 0: div r0.xy, v0.xyxx, cb0[2].xyxx 1: mad r0.zw, r0.xxxy, l(0.000000, 0.000000, 2.000000, 2.000000), l(0.000000, 0.000000, -1.000000, -1.000000) 2: mov r1.yz, abs(r0.zzwz) 3: div r0.z, cb0[2].x, cb0[2].y 4: mul r1.x, r0.z, r1.y 5: add r0.zw, r1.xxxz, -cb3[2].xxxy 6: mul_sat r0.zw, r0.zzzw, l(0.000000, 0.000000, 0.555556, 0.555556) 7: log r0.zw, r0.zzzw 8: mul r0.zw, r0.zzzw, l(0.000000, 0.000000, 2.500000, 2.500000) 9: exp r0.zw, r0.zzzw 10: dp2 r0.z, r0.zwzz, r0.zwzz 11: sqrt r0.z, r0.z 12: min r0.z, r0.z, l(1.000000) 13: add r0.z, -r0.z, l(1.000000)
No HLSL, podemos escrever isso da seguinte maneira:
Primeiro, o intervalo [-1; 1] UV e seus valores absolutos. Depois, há um "aperto" complicado. A máscara finalizada é a seguinte:Voltarei a esta máscara mais tarde.Agora vou pular intencionalmente algumas linhas de código e estudar cuidadosamente o código responsável pelo efeito de zoom. 22: add r1.xy, v0.xyxx, v0.xyxx 23: div r1.xy, r1.xyxx, cb0[2].xyxx 24: add r1.xy, r1.xyxx, l(-1.000000, -1.000000, 0.000000, 0.000000) 25: dp2 r0.y, r1.xyxx, r1.xyxx 26: mul r1.xy, r0.yyyy, r1.xyxx 27: mul r0.y, r0.w, l(0.100000) 28: mul r1.xy, r0.yyyy, r1.xyxx 29: max r1.xy, r1.xyxx, l(-0.400000, -0.400000, 0.000000, 0.000000) 30: min r1.xy, r1.xyxx, l(0.400000, 0.400000, 0.000000, 0.000000) 31: mul r1.xy, r1.xyxx, cb3[1].xxxx 32: mul r1.zw, r1.xxxy, cb0[2].zzzw 33: mad r1.zw, v0.xxxy, cb0[1].zzzw, -r1.zzzw
Primeiro, as coordenadas de textura "dobradas" são calculadas e a subtração flutuante2 (1, 1) é realizada: float2 uv4 = 2 * PosH.xy; uv4 /= cb0_v2.xy; uv4 -= float2(1.0, 1.0);
Esse texcoord pode ser visualizado da seguinte maneira:Em seguida , é calculado o ponto do produto escalar (uv4, uv4) , o que nos fornece a máscara:que é usado para multiplicar pelos cabos de texto acima:Importante: no canto superior esquerdo (pixels pretos) os valores são negativos. Eles são exibidos em preto (0,0) devido à precisão limitada do formato R11G11B10_FLOAT. Ele não possui um bit de sinal, portanto, valores negativos não podem ser armazenados nele.Em seguida, o coeficiente de atenuação é calculado (como eu disse acima, fisheyeAmount varia de 0,0 a 1,0). float attenuation = fisheyeAmount * 0.1; uv4 *= attenuation;
Em seguida, a restrição (max / min) e uma multiplicação são realizadas.Assim, o deslocamento é calculado. Para calcular o uv final, que será usado para provar a textura da cor, basta executar a subtração:float2 colorUV = mainUv - offset;Ao amostrar a textura de cor colorUV de entrada , obtemos uma imagem distorcida nos cantos:Esboços
O próximo passo é fazer uma amostra do mapa de contornos para encontrar os contornos. É bem simples, primeiro encontramos os cabos de texto para amostrar os contornos de objetos interessantes e depois fazer o mesmo para as faixas:
Objetos interessantes do mapa de contornoTraços do mapa de contornoVale a pena notar que nós coletamos apenas o canal .x do mapa de contorno e consideramos apenas os quadrados superiores.Movimento
Para implementar o movimento das faixas, quase o mesmo truque é usado como efeito de intoxicação. Um círculo do tamanho de uma unidade é adicionado e amostramos 8 vezes o mapa de contorno para objetos e traços interessantes, bem como a textura da cor.Observe que apenas dividimos os caminhos encontrados por 8.0.Como estamos no espaço das coordenadas de textura [0-1] 2 , a presença de um círculo de raio 1 para circundar um único pixel criará artefatos inaceitáveis:Portanto, antes de prosseguir, vamos descobrir como esse raio é calculado. Para fazer isso, precisamos retornar às linhas ausentes 15-21. Um pequeno problema no cálculo desse raio é que seu cálculo está espalhado pelo sombreador (possivelmente devido às otimizações do sombreador pelo compilador). Portanto, aqui está a primeira parte (15-21) e a segunda (41-42): 15: add_sat r1.xy, -r0.xyxx, l(0.030000, 0.030000, 0.000000, 0.000000) 16: add r1.x, r1.y, r1.x 17: add_sat r0.xy, r0.xyxx, l(-0.970000, -0.970000, 0.000000, 0.000000) 18: add r0.x, r0.x, r1.x 19: add r0.x, r0.y, r0.x 20: mul r0.x, r0.x, l(20.000000) 21: min r0.x, r0.x, l(1.000000) ... 41: add r0.x, -r0.x, l(1.000000) 42: mul r0.xy, r0.xyxx, l(0.030000, 0.125000, 0.000000, 0.000000)
Como você pode ver, consideramos apenas texels de [0,00 - 0,03] ao lado de cada superfície, resumimos seus valores, multiplicamos 20 e saturamos. Aqui está a aparência deles depois das linhas 15 a 21:E aqui está como após a linha 41:Na linha 42, multiplicamos isso por 0,03, esse valor é o raio do círculo para a tela inteira. Como você pode ver, mais perto das bordas da tela, o raio fica menor.Agora podemos ver o código do assembler responsável pelo movimento: 40: mul r3.x, cb0[0].x, l(0.100000) 41: add r0.x, -r0.x, l(1.000000) 42: mul r0.xy, r0.xyxx, l(0.030000, 0.125000, 0.000000, 0.000000) 43: mov r3.yzw, l(0, 0, 0, 0) 44: mov r4.x, r0.y 45: mov r4.y, r2.w 46: mov r4.z, l(0) 47: loop 48: ige r4.w, r4.z, l(8) 49: breakc_nz r4.w 50: itof r4.w, r4.z 51: mad r4.w, r4.w, l(0.785375), -r3.x 52: sincos r5.x, r6.x, r4.w 53: mov r6.y, r5.x 54: mul r5.xy, r0.xxxx, r6.xyxx 55: mad r5.zw, r5.xxxy, l(0.000000, 0.000000, 0.125000, 0.125000), r1.zzzw 56: mul r6.xy, r5.zwzz, l(0.500000, 0.500000, 0.000000, 0.000000) 57: sample_indexable(texture2d)(float,float,float,float) r4.w, r6.xyxx, t2.yzwx, s2 58: mad r4.x, r4.w, l(0.125000), r4.x 59: mad r5.zw, r5.zzzw, l(0.000000, 0.000000, 0.500000, 0.500000), l(0.000000, 0.000000, 0.500000, 0.000000) 60: sample_indexable(texture2d)(float,float,float,float) r4.w, r5.zwzz, t2.yzwx, s2 61: mad r4.y, r4.w, l(0.125000), r4.y 62: mad r5.xy, r5.xyxx, r1.xyxx, r1.zwzz 63: sample_indexable(texture2d)(float,float,float,float) r5.xyz, r5.xyxx, t0.xyzw, s0 64: mad r3.yzw, r5.xxyz, l(0.000000, 0.125000, 0.125000, 0.125000), r3.yyzw 65: iadd r4.z, r4.z, l(1) 66: endloop
Vamos ficar aqui por um minuto. Na linha 40, obtemos o coeficiente de tempo - apenas decorridoTime * 0,1 . Na linha 43, temos um buffer para a textura da cor obtida dentro do loop.r0.x (linhas 41-42) é, como sabemos agora, o raio do círculo. r4.x (linha 44) é o contorno de objetos interessantes, r4.y (linha 45) é o contorno de faixas (anteriormente dividido por 8!) e r4.z (linha 46) é o contador de loop.Como você pode esperar, o loop possui 8 iterações. Começamos calculando o ângulo em radianos i * PI_4 , o que nos dá 2 * PI - um círculo completo. O ângulo é distorcido ao longo do tempo.Usando sincos, determinamos o ponto de amostragem (círculo unitário) e alteramos o raio usando a multiplicação (linha 54).Depois disso, contornamos o pixel em um círculo e mostramos os contornos e cores. Após o ciclo, obtemos os valores médios (devido à divisão por 8) dos contornos e cores. float timeParam = time * 0.1;
A amostragem de cores será realizada da mesma maneira, mas adicionaremos um deslocamento multiplicado por um círculo "único" ao colorVV base .Brilho
Após o ciclo, amostramos o mapa de brilho e alteramos os valores finais de brilho (porque o mapa de brilho não sabe nada sobre os contornos): 67: sample_indexable(texture2d)(float,float,float,float) r0.xy, r1.zwzz, t3.xyzw, s0 68: mad_sat r0.xy, -r0.xyxx, l(0.800000, 0.750000, 0.000000, 0.000000), r4.xyxx
Código HLSL:
Cantos cinzentos e a unificação final de tudo
A cor cinza mais próxima dos cantos é calculada usando o produto escalar (linha de montagem 69):
Em seguida, duas interpolações seguem. A primeira combina cinza com a "cor no círculo" usando a primeira máscara que descrevi, para que os cantos fiquem cinza. Além disso, existe um coeficiente de 0,6, que reduz a saturação da imagem final:A segunda combina a primeira cor com a acima, usando fisheyeAmount . Isso significa que a tela fica gradualmente mais escura (devido à multiplicação de 0,6) e cinza nos cantos! Engenhoso.HLSL:
Agora podemos adicionar os contornos dos objetos.As cores (vermelho e amarelo) são retiradas do buffer constante.
Fuh! Estamos quase na linha de chegada!Temos a cor final, existe a cor do instinto da bruxa ... resta combiná-los de alguma forma!E para isso, a adição simples não é adequada. Primeiro, calculamos o produto escalar: 78: dp3_sat r0.x, r0.xyzx, l(1.000000, 1.000000, 1.000000, 0.000000) float dot_senses_total = saturate( dot(senses_total, float3(1.0, 1.0, 1.0) ) );
que se parece com isso:E esses valores no final são usados para interpolar entre a cor e o talento (saturado) da bruxa: 76: mad r0.xyz, r0.yzwy, l(1.200000, 1.200000, 1.200000, 0.000000), r2.xyzx 77: mov_sat r2.xyz, r0.xyzx 78: dp3_sat r0.x, r0.xyzx, l(1.000000, 1.000000, 1.000000, 0.000000) 79: add r0.yzw, -r1.xxyz, r2.xxyz 80: mad o0.xyz, r0.xxxx, r0.yzwy, r1.xyzx 81: mov o0.w, l(1.000000) 82: ret float3 senses_total = 1.2 * senses_traces + senses_interesting;
E isso é tudo.O shader completo está disponível aqui .Comparação dos meus shaders (esquerdo) e originais (direito):Espero que você tenha gostado deste artigo! Existem muitas idéias brilhantes na mecânica do "instinto bruxo", e o resultado final é muito plausível.[Partes anteriores da análise: primeiro e segundo .]