1. Introdução
Esta breve nota falará sobre como o modelo de dispersão da luz atmosférica está estruturado em nosso último 4k int Appear by Jetlag , cuja versão em festa conquistou um 12o lugar honroso na introdução da composição em 4k na festa de demonstração Revision 2018 em abril deste ano.
Você pode baixar o binário gratuitamente sem SMS aqui .
Se, no entanto, você não possui Windows ou se não possui uma placa de vídeo moderna e poderosa, existe um tolo reconfortante:
A música para este trabalho foi escrita por keen usando 4klang . Todo o código e recursos visuais ficaram para trás.
Aqui vamos falar apenas sobre o modelo de espalhamento de luz. Outras coisas, como ferramentas, um modelo de cidade, um modelo de iluminação e materiais, não são afetadas. Posso enviar os corajosos para ler a fonte ou assistir as gravações de como estou me afogando há horas - a maior parte do desenvolvimento foi em vídeo.
Uma história chata a perder
O trabalho nesse trabalho começou com a constatação de que o trabalho principal em tempo integral não deixa tempo para trabalhar em um trabalho completo de 4k - já é quase meados de março no pátio, faltam algumas semanas para o Revizen.
Resta apenas encontrar algo simples o suficiente para uma rápida, por algumas noites, enchimento de compo. Fazer outra marcha estúpida não é respeitar o espectador, então lembrei-me de que alguns anos atrás eu tinha que fazer um shader com espalhamento, e era bastante simples, compacto e ao mesmo tempo permitido, bonito, embora um tanto lento.
No decorrer de uma breve discussão, insisti por conta própria e decidimos nos concentrar no seguinte: criar uma paisagem cheia de luz difusa, com pôr do sol, nuvens e raios crepusculares (TIL como a expressão "raios divinos" é traduzida). Para não elevar o número de etapas na atmosfera a valores completamente não interativos, você precisa fazer um balanço forte (como o método do pátio de Monte Carlo), o que gera ruído visível. Mas não importa se você move a câmera e muda a cena lentamente e inicia a trilha ambiente, é possível misturar sem problemas os quadros adjacentes e suavizar temporariamente esse ruído.
Keen escreveu a música rapidamente - estava quase pronta duas semanas antes da revisão. No entanto, eu estava seriamente prejudicada pela gripe - com uma ambulância e uma doença infecciosa -, então praticamente não comecei a trabalhar no shader até o momento em que, numa condição de vida mais ou menos de alguma forma, peguei um avião para Frankfurt. O protótipo deste modelo de dispersão já estava escrito no ar.
Já tínhamos chicoteado a versão do intra da areia e da saliva na própria festa pelas poucas horas restantes antes do prazo (e, provavelmente, algumas depois; D), enquanto eu me afastava simultaneamente da gripe, falta de sono, voos de uma hora e ele estava constantemente distraído participando do compo de codificação ao vivo do Shader .
A versão mostrada na tela grande continha muitos artefatos e apenas a geometria rudimentar da cidade, com base no diagrama de Voronoi, com alturas aleatórias.
Em geral, o 12º lugar é bastante generoso.
A versão final, mostrada acima, foi feita mais tarde e em um modo mais descontraído, das 14h às 21h por semana, durante um mês. No total, foram necessárias entre 40 e 50 horas de trabalho.
Modelo de dispersão
(Observação: eu não faço programação gráfica profissionalmente. Esse é meu pequeno hobby aconchegante para o bem, se cem ou duas estiverem muito desfocadas sob cerveja horas de vinho por ano. Portanto, não há probabilidade zero de que algumas coisas sejam descritas abaixo e / ou nomeadas incorretamente. Tio, bata!)
O modelo de dispersão é emprestado do artigo "Espalhamento de luz externa de alto desempenho usando amostragem epipolar", de Egor Yusov , publicado no livro GPU Pro 5 , com amostragem epipolar completamente ejetada.
Modelo físico
Fótons solares bombardeiam a atmosfera da Terra e interagem com partículas de ar. Um fóton pode ser espalhado por uma partícula, o que implica uma mudança na direção do fóton, ou pode ser absorvido, o que significa que o fóton é perdido e sua energia foi convertida em alguma outra forma.
Ambos os processos são probabilísticos e dependem em particular da densidade de partículas e energia de fótons (que corresponde à sua cor).
Nos dedos, os fótons “vermelhos” têm uma chance menor de interagir com o ar, portanto superam a espessura da atmosfera relativamente intacta.
Os azuis, no entanto, têm uma maior probabilidade de dispersão, e é por isso que eles podem mudar de direção repetidamente e percorrer distâncias consideráveis na atmosfera antes de alcançar (ou não) o observador.

Os parâmetros da interação da luz com o ar que nos interessam são os seguintes:
- betas(x) - fração da luz dispersa por unidade de comprimento em um ponto x
- betaa(x) - fração da luz absorvida por unidade de comprimento em um ponto x
- betae(x)= betas(x)+ betaa(x) - fração total de luz perdida por unidade de comprimento em um ponto x
- p( alpha) É a distribuição angular da luz dispersa, onde alpha este é o ângulo entre o incidente e o feixe disperso
Supõe-se que o ar consista em dois tipos de partículas, cuja dispersão ocorre independentemente: moléculas (modelo de Rayleigh) e aerossóis (partículas esféricas relativamente grandes, dispersão de Mie na literatura em inglês). Os modelos diferem apenas em valores diferentes para os parâmetros acima.
Para ambos os modelos, acredita-se que a densidade das partículas correspondentes diminua exponencialmente com a altura: rho= rho0e− frachH onde rho0 - densidade ao nível do mar. Odds beta proporcional rho , e seus significados abaixo são dados para o nível do mar.
Rayleigh model
- pR( alpha)= frac316 pi(1+ cos2( alpha)) [Nishita et al. 93, Preetham et ai. 99]
- betaaR=0
- mathbf betaeR= mathbf betasR=(5.8,13.5,33.1)rgb10−6m−1 [Riley et al. 04, Bruneton e Neyret 08]
- HR=7994m [Nishita et al. 93]
Aerossóis
- pM( alpha)= frac14 pi frac3(1−g2)2(2+g2) frac1+ cos2( alfa)(1+g2−2g cos( alpha)) frac32 [Nisita et al. 93, Riley et ai. 04] onde g=0,76 [Bruneton e Neyret 08]
- betasM=2 cdot10−5m−1 [Bruneton e Neyret 08]
- betaeM=1,1 betasM
- HM=1200milhões [Nishita et al. 93]
Aproximação de espalhamento único
A aproximação da dispersão é baseada na emissão de um feixe de cada pixel da câmera e no cálculo de quanta luz da atmosfera deve receber dessa direção. Cada raio corresponde a todos os três componentes de luz RGB, como se três fótons com energias correspondentes voassem ao longo desse raio.
A luz que chega à câmara é formada pelos seguintes processos no ar:
- Dispersão (TIL que os figos aprendem a traduzir na dispersão). É adicionada a luz emitida pelo sol, que é probabilisticamente dispersa por um ângulo correspondente à direção da câmera.
- Absorção . A luz que já voa ao longo do feixe é absorvida pelo ar.
- Dispersão . A luz que já voa ao longo do feixe é perdida por se espalhar em outras direções.
Por motivos de desempenho, acreditamos que a luz só pode entrar na direção da câmera se espalhar uma vez, e todas as outras luzes (que foram espalhadas mais de uma vez) podem ser negligenciadas. Isso não é recomendado para o crepúsculo, mas o que fazer.
Essa abordagem é retratada na seguinte imagem bonita (eu tentei!):

Assim, a quantidade de luz que deve ser detectada pelo pixel da câmera em O pode ser calculado como a soma mathbfL= mathbfLin+ mathbfLBO onde mathbfLin - luz difusa do sol, e mathbfLBO - a quantidade de luz do ponto B cena geometria objeto atingindo O .
Geometria da luz
mathbfLBO= mathbfLOe− mathbfT(B rightarrowO) onde mathbfLO A luz emitida de um ponto B em direção à câmera.
mathbfT(B rightarrowO) chamada espessura óptica do meio entre os pontos B e O e é calculado da seguinte forma:
mathbfT(B rightarrowO)= intOB( betaeM(s)+ mathbf betaeR(s))ds
Considerando que os membros beta consistir na densidade constante e variável do nível do mar, esta expressão pode ser convertida em:
mathbfT(B rightarrowO)= betaeM cdot intOB rhoM(s)ds+ mathbf betaeR intOB rhoR(s)ds= barra mathbf beta cdot barraT rho(B rightarrowO)
Observe que eu não divulgo especificamente rho , porque os alteraremos posteriormente ao adicionar nuvens. Também chamo a atenção para o fato de que beta - vetores RGB (pelo menos mathbf betaR têm significados diferentes para os componentes RGB e betaM - vetor apenas por consistência). Membros com rho sob a integral estão escalares.
Luz do sol
Luz do sol mathbfLin calculado pela integração em todos os pontos P ao longo do segmento Ob e o acúmulo de toda a luz solar que entra na câmera e morre na espessura mathbfT(P rightarrowO) .
A quantidade de luz solar atingindo um ponto P é calculado por uma fórmula semelhante mathbfLP= mathbfLsune−T(A rightarrowP) onde mathbfLsun - o brilho do sol, e A É o ponto em que o raio do ponto P em direção ao sol vecs sai da atmosfera. A fração dessa luz que será espalhada na direção da câmera é mathbfLP cdot( mathbf betasR(s)pR( alpha)+ betasM(s)pM( alpha)) .
Total que obtemos:
mathbfLin= intOB mathbfLP(s) cdot( betasM(s)pM( alpha)+ mathbf betasR(s)pR( alpha)) cdote− mathbfT(P(s) rightarrowO)ds
Você pode perceber que:
- alpha é uma constante para cada raio de pixel da câmera (acreditamos que o sol está infinitamente distante e os raios dele são paralelos)
- Odds beta consistem em constantes do nível do mar e funções de densidade rho(s)
- Funções p( alpha) tem fatores comuns para ambos os processos de espalhamento
Isso permite converter a expressão em:
mathbfLin= mathbfLsun(1+ cos2( alpha))( frac frac14 pi frac3(1−g2)2(2+g2)(1+g2−2g cos( alpha)) frac32 betasM cdot mathbfIM+ frac316 pi mathbf betasR cdot mathbfIR)
onde
mathbfIM= intOB rhoM(s)e− mathbfT(A rightarrowP(s))− mathbfT(P(s) rightarrowO)ds
mathbfIR= intOB rhoR(s)e− mathbfT(A rightarrowP(s))− mathbfT(P(s) rightarrowO)ds
Im e IR diferem apenas nas funções de densidade; seus exponenciais são os mesmos.
Ninguém foi capaz de calcular essas integrais analiticamente, então elas precisam ser calculadas numericamente usando o re-mapeamento (como dizem nas publicações originais, você não pode fazê-lo!).
Integração numérica
Por razões de tamanho e preguiça, consideraremos o mais estúpido possível: intBAf(x)dx approx frac left|B−A right|N sumNi=0f(A+i cdot frac vecB−AN)
A marcha dos raios será realizada na direção oposta ao fluxo de luz: do ponto da câmera O antes da interseção da viga com geometria B . Segmento de linha O rightarrowB dividido por N etapas.
Antes de iniciar a marcha, inicialize as variáveis:
- espessura óptica total acumulada
vec2
(dois componentes separados, para espalhamento de Rayleigh e aerossol) mathbfT rho(P(s) rightarrowO) vec3
(RGB) mathbfIM , mathbfIR
Avançar para o ponto Pi cada passo entre O e B :
- Vamos raio vecs na direção do sol e conseguir um ponto Ai a saída desse raio da atmosfera.
- Calcular a espessura mathbfT(A rightarrowPi) calculando primeiro intPiA rhoM(s)ds e intPiA rhoR(s)ds usando a mesma nova marcação (com o número de etapas
M
) e multiplique os termos resultantes pelas constantes correspondentes betaeM e mathbf betaeR . - Calcular a espessura mathbfT rho(Pi rightarrowO)= mathbfT rho(Pi−1 rightarrowO)+ rhoi(s) cdotds
- Acumular mathbfIR e mathbfIM usando esses valores
A cor final após o reimarching é calculada pela soma dos termos:
- Prazo mathbfLBO é trivial: uma variável que contém mathbfT rho(Pi rightarrowO) contém valor mathbfT rho(B rightarrowO) desde Pi atingiu B .
- Por multiplicação mathbfIR e mathbfIM para as constantes correspondentes e adicionando o resultado é calculado mathbfLin
Shaders
Dispersão simples sem ninguém
Levemente penteado e comentado as fontes de dispersão extraídas (quase) diretamente do intra em si:
const float R0 = 6360e3; // const float Ra = 6380e3; // const vec3 bR = vec3(58e-7, 135e-7, 331e-7); // const vec3 bMs = vec3(2e-5); // const vec3 bMe = bMs * 1.1; const float I = 10.; // const vec3 C = vec3(0., -R0, 0.); // , (0, 0, 0) // // vec2(rho_rayleigh, rho_mie) vec2 densitiesRM(vec3 p) { float h = max(0., length(p - C) - R0); // return vec2(exp(-h/8e3), exp(-h/12e2)); } // , float escape(vec3 p, vec3 d, float R) { vec3 v = p - C; float b = dot(v, d); float det = b * b - dot(v, v) + R*R; if (det < 0.) return -1.; det = sqrt(det); float t1 = -b - det, t2 = -b + det; return (t1 >= 0.) ? t1 : t2; } // `L` `p` `d` // `steps` // vec2(depth_int_rayleigh, depth_int_mie) vec2 scatterDepthInt(vec3 o, vec3 d, float L, float steps) { vec2 depthRMs = vec2(0.); L /= steps; d *= L; for (float i = 0.; i < steps; ++i) depthRMs += densitiesRM(o + d * i); return depthRMs * L; } // ( -- ) vec2 totalDepthRM; vec3 I_R, I_M; // vec3 sundir; // , `-d` `L` `o` `d`. // `steps` -- void scatterIn(vec3 o, vec3 d, float L, float steps) { L /= steps; d *= L; // O B for (float i = 0.; i < steps; ++i) { // P_i vec3 p = o + d * i; vec2 dRM = densitiesRM(p) * L; // T(P_i -> O) totalDepthRM += dRM; // T(P_i ->O) + T(A -> P_i) // scatterDepthInt() T(A -> P_i) vec2 depthRMsum = totalDepthRM + scatterDepthInt(p, sundir, escape(p, sundir, Ra), 4.); vec3 A = exp(-bR * depthRMsum.x - bMe * depthRMsum.y); I_R += A * dRM.x; I_M += A * dRM.y; } } // // O = o -- // B = o + d * L -- // Lo -- B vec3 scatter(vec3 o, vec3 d, float L, vec3 Lo) { totalDepthRM = vec2(0.); I_R = I_M = vec3(0.); // T(P -> O) and I_M and I_R scatterIn(o, d, L, 16.); // mu = cos(alpha) float mu = dot(d, sundir); // return Lo * exp(-bR * totalDepthRM.x - bMe * totalDepthRM.y) // + I * (1. + mu * mu) * ( I_R * bR * .0597 + I_M * bMs * .0196 / pow(1.58 - 1.52 * mu, 1.5)); }
Cale a boca no shader
As nuvens
Nada mal, mas essa imagem também pode ser obtida muito mais facilmente com uma pilha de gradientes astuciosa.
De uma maneira enganosa, obter nuvens e raios divinos é muito mais difícil. Vamos adicionar.
A idéia é aproximar as nuvens com aerossóis e modificar apenas a função densitiesRM()
. Isso pode não ser tão fisicamente correto quanto gostaríamos (não tenho idéia de como a dispersão da luz nas nuvens na computação gráfica se aproxima).
// const float low = 1e3, hi = 25e2; // vec4 noise24(vec2 v) -- // float t -- float noise31(vec3 v) { return (noise24(v.xz).x + noise24(v.yx).y) * .5; } vec2 densitiesRM(vec3 p) { float h = max(0., length(p - C) - R0); vec2 retRM = vec2(exp(-h/8e3), exp(-h/12e2) * 8.); // () if (low < h && h < hi) { vec3 v = 15e-4 * (p + t * vec3(-90., 0., 80.)); // <s></s> : retRM.y += 250. * step(vz, 38.) * smoothstep(low, low + 1e2, h) * smoothstep(hi, hi - 1e3, h) * smoothstep(.5, .55, // : .75 * noise31(v) + .125 * noise31(v*4. + t) + .0625 * noise31(v*9.) + .0625 * noise31(v*17.)-.1 ); } return retRM; }
Ao contrário das expectativas, não temos nuvens bonitas, uma doce vitória e fãs, mas artefatos. Tentar aumentar o número de etapas dos artefatos na testa não remove completamente, mas prejudica significativamente o desempenho.
Soluções muletas que empurram o intra:
- Os artefatos mais desagradáveis do horizonte estão escondidos atrás das montanhas
- As nuvens são adicionadas apenas perto da câmera.
- Monte-Karlovschina é adicionado, cada raio de marcha é deslocado por um deslocamento aleatório:
for (float i = pixel_random.w; i < steps; ++i)
. Isso aumenta o ruído que você precisa suavizar temporariamente misturando quadros sucessivos. O número de etapas para zonas que exigem mais detalhes está aumentando (por exemplo, uma camada com nuvens). É para isso que é feita uma separação tão absurda de funções em scatterImpl()
e scatterDepthInt()
:
// scatterIn() vec2 depthRMsum = totalDepthRM; float l = max(0., escape(p, sundir, R0 + hi)); if (l > 0.) // 16 depthRMsum += scatterDepthInt(p, sundir, l, 16.); // 4- depthRMsum += scatterDepthInt(p + sundir * l, sundir, escape(p, sundir, Ra), 4.);
// scatter() // 10 float l = 10e3; if (L < l) scatterIn(o, d, L, 16.); else { scatterIn(o, d, l, 32.); // 8
Alinhamento com geometria da cena
Como resultado do mapeamento tradicional das funções de distância e sombra, a distância L
ao ponto B
e a cor do pixel Lo
já foram obtidas. Esses valores são simplesmente substituídos na função scatter()
. Se o feixe não descansar contra a geometria e sair da cena, a cor Lo
zero e L
calculada usando escape()
- acredita-se que o feixe tenha deixado a atmosfera.
Como tudo.
... De fato, é claro, nem todos. É muito difícil esfregar todas as partes para que, como um todo, pareça crível. Apenas um monte de confusão com parâmetros distorcidos, geometria da cena, funções de ruído, trajetória e ângulo da câmera. Receio não ter bons conselhos aqui, exceto por repetir várias horas e bater a cabeça contra a parede.
Minificação
Depois de processar o minificador de sombreador , o código de dispersão final do sombreador tem aproximadamente 1500 bytes de tamanho. O Crinkler o compacta em ~ 700 bytes, o que representa cerca de 30% de todo o código de sombreador.
Reprodução
Eu não sei como computação gráfica.