Implementando Raytracing usando XNA


Este post irá falar um pouco sobre um renderizador baseado em raytracing que eu implementei um tempo atrás usando XNA 3.1 e C#.

Iniciarei com uma introducão teória (fácil de acompanhar) e em seguida mostrarei alguns detalhes da implementação.

Introdução Teórica

A renderização por raytracing é bastante diferente da baseada em rasterizacão (veja este post http://ploobs.com.br/?p=622 para entender um pouco mais sobre rasterizacão ).

O algoritmo mais classico utilizado em raytracing, baseia-se na simulação do trajeto que os raios de luz percorreriam no mundo real, porém ao invés do raio sair da luz e chegar aos objetos (e eventualmente até a câmera), o nosso raio sairá da câmera e irá em direcão aos objetos. (fisicamente correto segundo as leias da ótica)

No mundo real, os raios de luz são emitidos a partir de uma fonte de luz, percorrendo o espaço até encontrar um objeto. Após os raios de luz atingirem o objeto, estes são refratados ou refletidos, de acordo com as características do objeto, nomeadamente, cor, textura e transparência, alterando assim a sua trajetória, e fazendo com que apenas uma infinitésima minoria dos raios que partiram da fonte de luz atinjam, por fim, os olhos do observador.

Como se pode perceber facilmente, implementar computacionalmente um sistema que simulasse o que foi descrito no parágrafo anterior é impraticável e um tanto quanto desnecessário (seria um desperdício já que a quantidade de raios de luz que atingem o olho do observador é extremamente pequena em comparacão com a quantidade de raios emitidos pelas luzes ). Assim sendo, o algoritmo ray tracing limita-se a inverter o sentido do trajeto dos raios de luz, passando agora o observador a ser a fonte ( apenas serão processados os raios que efetivamente são vistos pelo observador). Esta inversão de papéis tem seu custo, uma vez que não conseguimos simular com precisão a luz indireta que ilumina a cena.

Apesar da simplificacão feita, está técnica ainda é bastante pesada computacionalmente, portanto é apenas indicada para áreas como cinema, Tv, vídeos (em que precisamos de fotorrealismo e não estamos preocupados com velocidade).

Descrição do algoritmo

O algoritmo ray tracing é um algoritmo recursivo que consiste em projetar, a partir do observador (câmera), um vetor (raio) para cada um dos pixels da nossa cena/imagem, vetor este que irá interceptar os objetos que formam a cena.
O raio emitido pode:
  • Não interceptar nenhum objeto no seu trajeto, desta forma atribuimos ao pixel que o gerou a cor do background
  • No caso de o raio interceptar algum objeto, é necessário determinar a cor do pixel correspondente. Para tal é necessário calcular a iluminação no ponto da cena que o raio atinge, iluminação esta que pode ser proveniente diretamente de fontes de luz, pode ser luz proveniente de outro objeto que por reflexão ilumina o ponto que estamos a analisar, pode ser luz refratada transmitida através do objeto e que assim ilumina o ponto, ou pode ainda ser uma combinação de mais do que uma destas formas de iluminação.

Quando o raio encontra um objeto, são gerados três outros raios secundários:

Cada um destes raios possui características e objetivos diferentes. Estes raios secundários gerados irão novamente colidir com outros objetos … conforme mostra a imagem abaixo (omitidas as fontes de luz e os raios de sombra).

 

Como podemos observar na figura um único raio pode ser refletido e refratado indefinidamente, subdividindo-se em vários raios secundários, formando assim uma árvore de raios, mais ou menos, complexa, de acordo com a composição da nossa cena. Para conseguirmos processar esta informação, necessitamos de estabelecer um limite máximo de subdivisões. (normalmente estabelecemos uma profundidade máxima para esta árvore, ao alcancá-la, atribuímos a cor de background ao raio mais baixo da arvore e não o subdividimos)

Quando todos os raios chegarem aos seus destinos (seja atraves do criterio de máxima subdivisao ou por terem atingido o background), as suas contribuicões são calculadas do fim para o começo. A cada interseccão, utilizamos uma equacão (será vista mais a frente) para combinar os efeitos dos raios de sombra, reflexão e refracão. No fim teremos apenas um valor que será atribuído ao pixel que gerou o primeiro raio.

As sombras são verificadas através de raios secundários que são lançados a partir do ponto de intersecção do raio primário com o objeto (conforme visto na primeira imagem), em direção ao foco de luz. Se no seu trajeto o raio cruzar-se com um objeto, é porque o ponto em análise se encontra na sombra, se não, é porque recebe luz direta. Em miúdos =P, se tiver alguma coisa entre o ponto de interseccão e a fonte de luz, é porque o objeto está na sombra para esta luz.

O pseudo algoritmo abaixo mostra um pouco da dinâmica do sistema:

Criar um raio a partir do ponto de visão (observador) e que passe pelo PixelAtual
(Para cada objeto da cena)
  {
     Se o raio intercepta o ObjetoAtual e esta é a intersecao mais proxima
     {

      Lançar um novo raio na direcao de cada fonte de luz (raios de sombra)
       Se a superfície é reflectora
       {
          gerar um raio refletor (recursivo)
        }
        Se a superfície é transparente
       {
        gerar um raio refrator (recursivo)
       }
       Devolver a combinacao entre os valores obtidos dos raios de sombra, raio refletor e raio refrator
}
   Se Nao encontrou nenhum objeto
{
      Preencher PixelAtual com a cor do fundo
}
 }

Implementação

A implementacão feita não inclui raios refratados (se alguém quiser se aventurar =P boa sorte, não é uma tarefa difícil não)

O metodo a seguir é chamado a todo frame (removi algumas linhas sobre suporte a multiThread que não ajudavam em nada na compreensão da técnica).

Implementei um pequeno antialiasing (tente desabilitá-lo e veja como que a imagem fica horrível). Usei algo próximo do SuperSample (por pixel eu jogo diversos raios seguindo um padrão e uso a média entre eles).

public void Draw()
{
            for (int y = 0; y <  ScreenSizeY; y++)
		{
                         for (int x = 0; x < ScreenSizeX; x++)
	           	{				

					Ray tPrimaryRay = _camera.GetUnprojectedRay(x, y);
					FloatColour tColour = TraceRay(null, tPrimaryRay, 0,0);

                                       ///filtragem
                                        if (StaticContent.isAntialiasing == true)
                                       {
                                         float[] antiAliasingDistances = StaticContent.AntiAliasingDistances;
                                         Vector2[] pattern = StaticContent.Pattern;
                                         for (int i = 0; i < antiAliasingDistances.Length; i++)
                                        {
                                           for (int j = 0; j < pattern.Length; j++)
                                            {
                                                tPrimaryRay = _camera.GetUnprojectedRay(x + pattern[j].X * antiAliasingDistances[i], y + pattern[j].Y * antiAliasingDistances[i]);
                                                tColour += TraceRay(null, tPrimaryRay,0, 0);
                                             }
                                         }

                                         Image[x, y] = tColour / (antiAliasingDistances.Length * pattern.Length + 1);
                                          }
                                          else
                                          {
                                            Image[x, y] = tColour ;
                                          }
			}
		}

	}

O método que cria os raios para cada pixel na tela pode ser visto a seguir:

public Ray GetUnprojectedRay(float x, float y)
        {
            Vector3 nearSource = new Vector3(x, y, 0);
            Vector3 farSource = new Vector3(x, y, 1);
            Vector3 nearPoint = StaticContent.GraphicsDevice.Viewport.Unproject(nearSource, Projection, View, Matrix.Identity);
            Vector3 farPoint = StaticContent.GraphicsDevice.Viewport.Unproject(farSource, Projection, View, Matrix.Identity);
            Vector3 direction = Vector3.Normalize(farPoint - nearPoint);
            return new Ray(nearPoint, direction);
        }

A idéia é simples:

  1. Sabemos que este raio sai da camera e atinge os objetos
  2. Sabemos que este raio (em projecao perspectiva)  em espaco de projecão corresponde a uma reta
  3. Intuitivamente, todos os objetos que interceptam este raio, quando projetados ocuparão o mesmo pixel (tente lancar um raio do seu olha pra alguma direcao, voce percebera que voce vera apenas o primeiro interceptado, o segundo estaria projetado na mesma posicao do segundo, o terceiro idem … em um render tradicional por rasterizacao, o ZBuffer resolve quem deve ficar na frente de quem =P)
  4. Basta então criar dois pontos com o mesmo X,Y (e qualquer Z) em espaco de projecao e “desprojetá-los ” (multiplicar pela inversa da Matriz View * Projection) e teremos os pontos em espaco World, em seguida construimos um vetor entre eles e temos o nosso raio procurado =P

OBS: lembre-se que espaco de projecão normalizado, o X,Y correspondem a posicão do Pixel na tela normalizada e o Z corresponde a distancia “normalizada” dele em relacão a câmera.

A funcão a seguir mostra como calculamos a contribuicão de cada um dos raios.

public FloatColour TraceRay(Primitive pIgnorePrimitive, Ray tRay, int nDepth , float acumulatedDistance)
		{
			    if (nDepth > StaticContent.MaxDepth)
			{
				return StaticContent.BackGround;
			}

			float fClosestIntersectionDistance = float.MaxValue;
			Primitive closestIntersectedPrimitive = null;

			foreach (Primitive primitive in m_pScene.Primitives)
			{

				float fDistance;
				if (primitive.Intersect(tRay, out fDistance))
				{

					if (fDistance < fClosestIntersectionDistance)
					{

						fClosestIntersectionDistance = fDistance;
						closestIntersectedPrimitive = primitive;
					}
				}
			}

			FloatColour tColour = StaticContent.BackGround;
			if (closestIntersectedPrimitive != null)
			{
				tColour = closestIntersectedPrimitive.Material.Shade(this, tRay, fClosestIntersectionDistance,acumulatedDistance, nDepth + 1);
			}

			tColour = FloatColour.Min(tColour, new FloatColour(1));

			return tColour;
		}

Verificamos se atingimos a subdivisão máxima, em seguida procuramos a primeira interseccão, caso exista chamamos o método Shade do material, caso não existe atribuímos a cor de BackGound. (checamos também para ver se a cor não está saturada).

O metodo primitive.intersect checa se o raio intercepta a primitiva em questão, cada Primitiva (como triangle, sphere, box …) contem uma implementacao diferente. Eu não otimizei nada, usei as equacões clássicas sem estrutura de aceleracão nenhuma (Triangle Mesh DEMORA, tá BRUTAL force total). Quem estiver interessado, procure sobre KD-Tree e/ou Octres para acelerar a colisão com triangles meshes =P

O método abaixo mostra como que o Shading é feito.

public virtual FloatColour Shade(RayTracer tracer, Ray tRay, float fDistance, float totalDistance ,int nDepth)
        {
            FloatColour tColour = new FloatColour(0, 0, 0);            

            Vector3 tIntersection = tRay.Position + tRay.Direction * fDistance;
            Vector3 tNormal = prim.CalculateNormal(tIntersection);

            Ray tReflectedRay;
            tReflectedRay.Direction = Vector3.Reflect(tRay.Direction, tNormal);
            tReflectedRay.Position = tIntersection;

            // add lighting components
            foreach (Luz light in tracer.Scene.Luzes)
            {
                // get vector from surface to light
                Vector3 tSurfaceToLight = light.GetDirectionToLight(tIntersection);
                float fDistanceToLight = tSurfaceToLight.Length();
                tSurfaceToLight.X /= fDistanceToLight;
                tSurfaceToLight.Y /= fDistanceToLight;
                tSurfaceToLight.Z /= fDistanceToLight;

                Ray tRayToLight;
                tRayToLight.Position = tIntersection;
                tRayToLight.Direction = tSurfaceToLight;
                float fLightFactor = 0;
                float fSpecular = 0;

                if (tracer.IsShadowed(prim, tRayToLight, fDistanceToLight))
                {
                    fLightFactor = StaticContent.AmbientLightContribuition;
                }
                else
                {
                    fLightFactor = Vector3.Dot(tSurfaceToLight, tNormal);
                    if (fLightFactor < 0) fLightFactor = 0;
                    fSpecular = Vector3.Dot(tRayToLight.Direction, tReflectedRay.Direction);
                    if (fSpecular > 0)
                    {
                        fSpecular = (float)Math.Pow(fSpecular, Shininess) * SpecularCoefficient;
                    }
                    else
                    {
                        fSpecular = 0;
                    }
                }
               tColour += (getColor(tIntersection) * fLightFactor + new FloatColour(fSpecular)) * light.Color * light.GetIntensity(tIntersection);

            }

            if (Reflectivity != 0)
            {
                FloatColour tReflectedColour = tracer.TraceRay(prim, tReflectedRay, nDepth + 1, totalDistance + fDistance);
                if (StaticContent.isRayAttenuationWithDistance)
                {
                    tColour += tReflectedColour * Reflectivity * Math.Min(1, StaticContent.RayAttenuation / (totalDistance + fDistance));
                }
                else
                {
                    tColour += tReflectedColour * Reflectivity;
                }

            }            

            if (tColour.R > 255) tColour.R = 255;
            if (tColour.G > 255) tColour.G = 255;
            if (tColour.B > 255) tColour.B = 255;

            return tColour;
        }

Eu me baseei fortemente no modelo de Phong para criar este método. Fiz algumas alteracões fisicamente incorretas, porém com resultados visuais melhores como a atenuacão pela distancia dos raios refletidos. Observe que quando estou chamando tracer.Ray, estou fazendo uma chamada recursiva, uma vez que foi um tracer.Ray que chamou este método Shade.

A implementacão disponível no repositório é multiThread (dá pra ver cada parte da tela sendo desenhado =P), faz correcão de Gama (não comentei sobre isso neste artigo) e têm Antialiasing.

Projeto no sourceforgehttp://sourceforge.net/projects/learnraytracing/

A nVidia, um tempo atrás anunciou um RayTracer interativo (Real-Time). Vale a pena conferir http://www.nvidia.com/object/io_1249366628071.html os resultados.
É isso pessoal, até a próxima =P

 

, , , , ,

  1. #1 by same day loan no denial on 22 de junho de 2017 - 10:56 am

    Cash is loaned to individuals who while waiting for the next
    pay period are experiencing cash emergencies. Senator Scott Sifton, D-Affton, said he was concerned about people who
    got loans from multiple lenders, which could perpetuate debt.

    Tell the caller you know it’s a scam and that you’re recording the telephone call and will turn it over to the
    authorities.

  2. #2 by Babyschale Test on 22 de junho de 2017 - 10:56 am

    Very neat post.Really looking forward to read more. Really Great.

  3. #3 by web site on 22 de junho de 2017 - 11:03 am

    just beneath, are several totally not related sites to ours, having said that, they’re certainly worth going over

  4. #4 by google.com on 22 de junho de 2017 - 11:06 am

    I love this film because it takes Indiana Jones all over the
    world so the settings change and you get different types of action scenes
    throughout the entire movie. 3 Princess Mononoke – Preview Princess Mononoke Wallpapers.
    The second scene in question is the one where Grey saves Ana from an
    oncoming biker and they again almost kiss.

  5. #5 by خرید اپل ایدی on 22 de junho de 2017 - 11:23 am

    I love the valuable info an individual present for your content. I most certainly will save your blog post and examine once more below typically.. خرید اپل ایدی I am somewhat guaranteed I’ll discover a good amount of brand new products right here! Best of luck for one more!

  6. #6 by پنجره دوجداره وین تک on 22 de junho de 2017 - 11:52 am

    Wow, great article post.Really looking forward to read more. Much obliged.

  7. #7 by portamur.ru on 22 de junho de 2017 - 12:03 pm

    Hi there, yeah this paragraph is in fact pleasant and I
    have learned lot of things from it regarding blogging.

    thanks.

  8. #8 by adam and eve on 22 de junho de 2017 - 12:04 pm

    we came across a cool web site that you just may well delight in. Take a appear should you want

  9. #9 by schauen sie sich um diese stelle herum on 22 de junho de 2017 - 12:35 pm

    Daher verfügen die meisten Polsterbetten über einen Bettkasten, der sich unterhalb der Matratze befindet.

  10. #10 by Finance News Websites on 22 de junho de 2017 - 12:36 pm

    Wow, awesome blog structure! How long have you ever been blogging for? you make running a blog look easy. The full look of your site is great, as smartly as the content!

  11. #11 by real swords on 22 de junho de 2017 - 12:36 pm

    AHHHH I LOVE IT!!!! Very Beautiful and arrived 2-3 weeks earlier then expected. Will definitely buy again

  12. #12 by porn gifs on 22 de junho de 2017 - 1:01 pm

    I value the post.Thanks Again. Awesome.

  13. #13 by Japanese Tsuka on 22 de junho de 2017 - 1:02 pm

    It was shipped quickly and arrived well packaged and protected.

  14. #14 by Real Nodachi on 22 de junho de 2017 - 1:03 pm

  15. #15 by car computer exchange on 22 de junho de 2017 - 1:57 pm

    I am really enjoying the theme/design of your website. Do you ever run into any browser compatibility issues?

    A small number of my blog readers have complained about my website not operating correctly in Explorer but looks great in Chrome.

    Do you have any recommendations to help fix this problem?

  16. #16 by Vpsgol.Net on 22 de junho de 2017 - 2:12 pm

    I love the techniques a person present in your articles. Let me bookmark your current blog page look yet again below usually. I’m just very sure I’m going to be up to date numerous fresh things below! All the best .. سرور مجازی ارزان for the!

  17. #17 by دانلود on 22 de junho de 2017 - 2:33 pm

    Only a smiling visitant here to share the love (:, btw great style.

  18. #18 by katie on 22 de junho de 2017 - 2:52 pm

    where to buy a c e diet pills

  19. #19 by pflum on 22 de junho de 2017 - 2:57 pm

    Hi, just became alert to your own website as a result of Search engines, and located it is absolutely informative.. سرور مجازی آلمان I will be thorough for the town. I’ll be grateful just in case you progress this particular later on. Many individuals can be had good results away from your writing. All the best!

  20. #20 by http://horowitz333.dothome.co.kr/?document_srl=2684 on 22 de junho de 2017 - 3:14 pm

    Hey there! I’ve been reading your weblog for some time now and finally
    got the bravery to go ahead and give you a shout out from
    New Caney Texas! Just wanted to tell you keep up the fantastic work!

  21. #21 by clit on 22 de junho de 2017 - 3:31 pm

    What’s up, I read your blog regularly. Your story-telling style is awesome, keep up
    the good work!

  22. #22 by dulin on 22 de junho de 2017 - 3:38 pm

    We have been a bunch associated with volunteers and cracking open a new design within our online community سرور اختصاصی. Your site provided us with helpful tips to operate for. You must have done the strong employment plus each of our whole community may be gracious to you.

  23. #23 by Naginata on 22 de junho de 2017 - 4:05 pm

    Cold steel awesome products like always. That is why I am a big fan of cold steel, they are a little high but have never been unhappy with the product.

  24. #24 by Washingtondc on 22 de junho de 2017 - 4:15 pm

    Generally I do not learn article on blogs, but I
    wish to say that this write-up very compelled me to take a look at and
    do so! Your writing taste has been amazed me. Thanks, quite
    nice post.

  25. #25 by nike air max on 22 de junho de 2017 - 5:13 pm

    I truly appreciate this blog.Thanks Again. Cool.

  26. #26 by best plumbing contractors on 22 de junho de 2017 - 5:26 pm

    The details talked about in the article are several of the very best accessible

  27. #27 by https://pt.wikipedia.org/wiki/Antônio_Carlos_de_Arruda_Botelho on 22 de junho de 2017 - 6:04 pm

    These are really impressive ideas in on the topic of blogging.

    You have touched some pleasant points here. Any way keep up wrinting.

  28. #28 by web Design on 22 de junho de 2017 - 6:11 pm

    Very nice post. I just stumbled upon your blog and wanted to say that I’ve truly enjoyed surfing around your blog posts. In any case I will be subscribing to your rss feed and I hope you write again soon!

  29. #29 by haben sie einen blick auf diese jungs on 22 de junho de 2017 - 7:02 pm

    Bei Aquarien mit über 70 cm Wasserstand empfiehlt es sich, die Leuchten mit 400 Watt HQI auszustatten.

1 618 619 620
(não será publicado)