- António Rodrigues - a22202884.
https://github.com/HienaDev/Collision-Shader
Comecei por procurar o “óbvio” e pesquisar como fazer um shader que reagia a colisões, com isso encontrei dois videos:
Shader Graph Forcefield: Update - Acabei por sentir que este vídeo não fazia o que queria, usava raycasts para determinar os impactos, e não colisões, mas retirei de lá a ideia de usar um ripple effect.
Energy Shield Effect in Unity URP Shader Graph - Este fazia quase tudo o que queria e usei como referência para começar.
Depois disto achei que procurar por um ripple effect seria o melhor começo, pois a partir daí, caso conseguisse determinar a posição das colisões, poderia enviar essa informação para o shader, e causar os ripple effects no local da colisão.
Pesquisei então por ripple effects e encontrei este video:
Shockwave Shader Graph - How to make a shock wave shader in Unity URP/HDRP
Este video foi um bom começo, e permitiu-me chegar a um resultado parecido ao efeito que queria:
Demonstração do efeito: Vídeo
Mas depois disto senti que tinha pouco controlo, no primeiro que era específico para sprites senti-me com mais controlo pois o shader permitia mudar o focal point num espaço 2D facilmente, e consegui também fazer um efeito com mais que um ripple:
Demonstração do efeito: Vídeo
Mas no caso da esfera, senti que tinha pouco controlo, e mesmo depois de algumas mudanças, e alterando o focal point para um Vector3, eu conseguia controlá-lo facilmente no eixo do X e do Y, mas estava com dificuldades no eixo do Z, garantidamente que era por não ter entendido tudo o que retirei do vídeo anterior, mas decidi voltar a pesquisar.
Com isso encontrei este vídeo: Unity Shader Graph VFX - Bubble Shield (Tutorial)
Com este tutorial aprendi algus efeitos como o twirl, que achei que seriam interessantes para o shader final, mas este tutorial usava uma esfera com UVs alterados, e eu quero criar um shader que funcione com qualquer objeto, especialmente os default do Unity:
Apesar de o material estar a mudar, a esfera em si não mudava, suspeitei que fosse por a esfera do vídeo ter UVs alterados, não tinha Maya como no vídeo mas instalei Blender para confirmar a teoria, após criar a esfera com os UVs como no tutorial, a esfera continuou sem deformação, acabei por não entender o porquê:
Mas depois disto achei que já tinha material suficiente para começar o meu shader.
Criei um temporizador que me permite ir de 0 a 1 com a função seno:
Em vez de usar diretamente o sine time, usamos o multiply pelo meio, para podemos alterar a frequência do time, e depois fazemos o sine, assim mantemos o intervalo entre 0 e 1 mas mudamos quanto oscila por segundo.
Criei isto com o intuito de começar por ter um bubbling effect, então a oscilação constante do sen era perfeita para isso.
Depois disso criei um grupo para mudar a posição dos vértices: para isso multiplico o resultado do grupo anterior pela normal dos vértices, depois adiciono essa mudança a posição de cada vértices, que vai ser na direção perpendicular ao vértices (o vetor normal de cada vértice), criando este efeito de expansão:
(Desenho que exemplifica a direção de cada vetor normal para cada vértice)
Com isto consegui o efeito pretendido:
Demonstração do efeito: Vídeo
Usando agora alguns componentes que vi serem usados num dos vídeos, criei o anel que percorre a mesh:
Recebemos o valor para a progressão da onda, ou seja onde está o anel, e usamos o componente fraction que nos dá a parte decimal do valor, depois pegamos neste valor e adicionamos-lhe o size, e noutro node subtraímos o size a ele, um exemplo disto: caso o valor estivesse no 0.6, e o size fosse 0.1, teríamos os valores 0.7 e 0.5, tendo assim uma wave de tamanho 0.2:
Depois disso, crio uma secção à parte, esta secção recebe o ponto de impacto (FocalPoint), normaliza este vetor para obter a direção.
Neste caso, como estamos a aplicar uma esfera de tamanho 1, dividimos por 2, pois queremos o comprimento de vetor a 0.5, já que o raio da esfera seria 0.5 e o diâmetro 1, as esferas neste caso teriam de ter sempre tamanho 1.
Depois subtraímos a direção pela posição no objeto, para termos a posição inicial da shockwave no objeto. O node da length dá-nos a distância até esta posição inicial no objeto:
Criei um node smoothstep para receber estes inputs, ao receber o add e o subtract, obtemos os limites do nosso anel e a length dá-nos o ponto inicial a partir de qual o anel se afasta e expande ao longo da mesh:
Dou este resultado a um One Minus node, como o smoothstep vai estar sempre compreendido entre 0 e 1, o one minus acaba por nos dar a diferença do seu input. Dando o valor oposto ao smoothstep.
Quando multiplicamos os dois resultados, tudo o que está a 0 em ambos é removido no outro, e onde não houver zeros, ficamos com um gradiente, que é mais forte no centro (onde ambos os inputs têm valor mais alto):
Multiplicamos este valor pela nossa amplitude, o que altera o tamanho dos nossos valores que não são zero, fazendo assim com que a onda aumente ou diminua de tamanho:
Finalmente, damos este resultado ao nosso grupo que altera a posição dos vértices e obtemos a deformação:
Demonstração do efeito: Vídeo
Temos agora o efeito pretendido, mas não o queremos a repetir com o tempo como está agora, ou seja o valor da progressão da onda vai deixar de ser oscilante, mas sim um valor que o programa controla, e queremos que comece onde haja colisões, para isso temos que criar um script que trate de dar os valores corretos ao shader.
Antes disso, criei um subshader mais organizado para o efeito. Este subshader recebe 4 variaveis:
- Progression: Distância do ponto de impacto entre 0 e 1, em que 0 é no ponto de impacto e 1 o ponto final;
- Focalpoint: O ponto inicial de impacto;
- Amplitude: A altura da onda;
- Size: O tamanho da onda;
O subshader devolve o anel para ser desenhado.
Agora com o subshader feito, vamos criar o script que deteta as colisões e dá os dados da mesma ao shader:
No nosso script temos 6 variáveis:
- material: O material do nosso objeto, que neste caso tem que ter o shader criado anteriormente;
- defaultFocalPoint: Esta variável é criada como a posição nula, para quando a onda termina, voltarmos a posição inicial para evitar deformações;
- maxFocalPoints: Isto define quantas ondas permitimos ao mesmo tempo no nosso objeto, terá sempre um hard limit definido pelo shader;
- index: Quando temos mais que uma onda, o index permite percorrer cada onda para ser desenhada, caso cheguemos ao limite definido pelo maxFocalPoints, a primeira onda será substituida pela nova onda;
- destroyCollidedObjects: Um booleano que da a opção ao utilizar de destruir os objetos que colidem com o objeto ou não;
- frequency: Quão rapido a onda se propaga.
Método Start
Assim que o programa corre, recebemos o material no objeto, definimos o defaultFocalPoint para dar reset as ondas, e damos este defaultFocalPoint para o focal point de cada onda para que comece tudo sem ondas. Inicializamos também o índice a 0.
Método Update
Durante o update, percorremos todos as ondas possíveis, crio uma variavel auxiliar para guardar a progressão da onda atual.
Depois verifico se esta progressão está compreendida entre 0 e 1, se sim, incrementamos e multiplicamos a incrementação pela frequencia e pelo Time.deltaTime para que seja constante entre dispositivos, e não depender da frame rate.
Depois atualizamos o valor da progressão da onda atual por este valor.
Caso a progressão seja maior ou igual a 1, voltamos a pôr a onda na posição inicial, e mudamos a progressão para 0 que é também o valor inicial da progressão
Deteção de colisões:
Quando detetamos uma colisão, recebemos todos os contact points desta colisão, e onde eles ocorrerem queremos iniciar uma onda.
Para isso, usamos o InverseTransformPoint que nos dá a posição local, em relação ao transform do objeto em que o script está, do ponto de colisão. Damos este posição ao primeiro FocalPoint disponível, que no caso duma primeira colisão seria o 0.
Uso aqui também a expressão "index % maxFocalPoints" que me dará apenas o resto da divisão pelos focalPoints, garantido assim que apenas verificaremos o número de waves maximas definidas pelo script. Se por exemplo tivéssemos maxFocalPoints = 3, teríamos sempre os valores 0, 1 e 2.
Depois disto inicializo o valor da progressão a 0.001, para que o Update saiba que tem que começar a incrementar a progressão naquele nível.
Pensei também que poderia ser necessário, caso o programa corresse durante muito tempo, haver uma verificação pela valor máximo de int, caso houvesse 2147483647 colisões, mas para esta situação assumi que isto seria um caso extremamente raro, então deixei apenas comentado para que não fosse completamente descartado.
Decidi dar a opção ao utilizador se quer que os objetos colididos sejam destruídos ou não.
Depois disto, para que o shader permitisse mais que uma onda, tive que criar mais variaveis:
Decidi que o shader permitira um máximo de 7 ondas simultâneas:
Criei também uma nova esfera com mais triangulos com probuilder, para poder ter waves mais pequenas, no caso da esfera default do unity, se ficasse muito pequena, levantava apenas um vértice e ficava um conjunto de pirâmides em vez de uma onda:
Unity Default Sphere:
ProBuild Sphere:
Depois disso adaptei o shader a nova esfera e fiquei com este efeito com as 7 ondas (Esfera do ProBuilder à esquerda e Esferda do Unity à direita):
Demonstração do efeito: Vídeo
Demonstração do efeito com rotação: Vídeo
Efeito escudo:
Em seguida decidi que já estava farto dos tijolos, então comecei a trabalhar para fazer um efeito para o escudo, primeiro deparei-me com este tutorial:
Unity Shader Graph - Sci-Fi Barrier / Shield Tutorial - Repliquei o tutorial e percebi melhor como funcionava o ruído e os padrões procedimentais.
Depois de muitos testes e exprimentações criei o seguinte shader:
Criei um node com o padrão Truchet, que recebe duas variaveis que apenas controlam o tiling e complexidade do padrão, depois multiplico este padrão por uma cor para obter a cor do escudo:
Depois criei um node de ruído Voronoi, que recebe a variavel CellDensity que controla quantas celulas o ruído tem. Depois temos duas variaveis:
- NoiseRotationSpeed: controla quão rapido o ruído se mexe no eixo do X ao longo do tempo;
- CellMovementSpeed: controla a velocidade ao longo do tempo da rotação que as celulas fazem em sua volta.
Por fim multiplico o padrão colorido pelo ruído e fico com o shader final:
Demonstração do efeito: Vídeo
Cor na onda:
De seguida queria adicionar uma cor à onda, assim para além da deformação fisica, dava uma certa "deformação" da cor.
Para isto multipliquei as ondas recebidas por uma cor que pode ser definida pelo utilizador, que por usa vez pode ser multiplicada por uma itensidade para que esta cor seja mais distinguível, e depois adiciono esta cor ao efeito previamente criado:
Intersecção:
Com isto feito, queria tentar fazer um efeito de intersecção da esfera com objetos na cena. Para isso encontrei este video:
Unity Shader Graph - Intersection Effect Tutorial
Resultado após o tutorial:
Apesar de conseguir replicar o efeito no video, não consegui aplicá-lo ao meu shader, decidi então deixar aqui como honorable mention e repliquei o efeito pretendido com duas esferas, uma com o shader de intersecão e outra com o shader de ondas:
Efeito com 2 esferas: Vídeo
No dia seguinte, por sorte, apareceu-me este vídeo, que mostrava um "museu" de shaders, e neste vídeo tinha o shader de intersecção outra vez, mas que funcionava de outra maneira: 10 Shaders in 10 Minutes - Unity Shader Graph
Com este vídeo, consegui finalmente fazer a intersecção no meu shader, ja que este recebia a base color, e aplicava o efeito por cima da base color:
Primeiro comparamos a profundidade da cena, com o node scene depth este node dá-nos a profundidade de cada objeto que esta a ser renderizado, e usando a sample eye vemos esta profundidade da prespetiva da câmara.
Depois removemos a este valor a posição do objeto com o node screen position, e ficamos com um "buraco" na prespetiva da câmara, depois ao invertermos isto com um one minus node ficamos apenas com este "buraco" ficando com a posição do objeto.
A variável IntersectionDepth, permite regular o tamanho da nossa interseção, usamos o node remap, que faz a nossa escala que seria de 0 a 1, passar a ser de 1 a 0, para que faça mais sentido, pois assim ao aumentar o valor da variavel, o tamanho da interseção aumenta:
Tudo até este ponto tinha visto no vídeo anterior, é neste novo vídeo que foi introduzida a diferença, ao invês de receber o valor alpha da cor e o inserir no campo alpha do nosso shader, que removia a cor do resto do escudo (o que fazia o efeito não funcionar, pois removia o escudo por inteiro), este shader faz um lerp, ou seja, onde for transparente, fica com a cor do escudo, caso não seja, fica a cor da interseção.
A variavel IntersectionStrenght permite-nos controlar o node power que nos dá o valor elevado a esta IntersectionStrength, que nos dá mais controlo sobre a intensidade do escudo:
Por fim, o ruído e cor da intersecção, este ruído é extremamente parecido com o ruído do escudo em si em termos de lógica, então não acho que precise de grande explicação, tem apenas algumas variaveis para controlar quão intenso é o movimento, e quão denso o ruído é, depois, como tinha referido, tudo isto é dado a um lerp, que faz então a interpolação como referido anteriormente:
Efeito com a intersecção final: Vídeo
O único ponto negativo é que com este shader, a cor do escudo é substituida pela da intersecção, ao invés de ficar por cima:
Efeito antigo:
Novo efeito:
Para terminar fiz uma demo scene a mostar o shader em diferentes cenários e diferentes padrões e efeitos que se consegues com o shader:
Shader final com o nome ShieldCollisionEffect:
Fazer o shader funcionar com qualquer mesh:
Até este ponto achava que estava concluído e estava a acabar o relatório, mas estava determinado a perceber porque não funcionava em mesh's diferentes de esferas, em cubos funcionava, mas mal.
Inicialmente achei que era porque os cubos têm apenas 6 vértices, e era verdade que era por isso que a deformação da mesh era estranha, porque só deformava nos cantos:
Cubo a deformar apenas nos cantos: Vídeo
Depois voltei ao meu subshader de impacto e comecei a mexer em alguns valores, e mudei a divisão para 1.5:
Depois disto a onda ficou quase perfeita nos cantos do cubo, mas no centro não:
Cubo com onda boa nos cantos: Vídeo
Lembrei-me que nesta face do cubo, temos um quadrado e caso o cubo tivesse 1 de tamanho (que é o caso), estas seriam as medidas:
Logo nos cantos a distância seria aproximadamente 0.7, e quando dividimos 1 por 1.5 temos 0.67, que é bastante perto de 0.7.
Isto fez-me perceber que o problema poderia estar aí, pensei inicalmente que cada mesh precisaria de uma divisão diferente, e que isto funcionaria apenas para esferas porque são as unicas com o mesmo tamanho em todas as direções.
Apesar de achar que normalizar o vetor era necessário para obter a direção, decidi remover este componente, já que esta divisão me estava a dar problemas, e só era necessária por causa da normalização, que no final de contas estava apenas a por os vetores todos com o mesmo tamanho, que não era ideal para qualquer objeto que não uma esfera.
Liguei o focal point diretamente a subtração da posição no objeto e agora funcionava em qualquer mesh:
Efeito em qualquer mesh: Vídeo
Agora tinha um novo problema:
Como visto no vídeo anterior a onda não propaga até ao final do objeto, apercebi-me que, de novo, ambos no shader de impacto e no script, eu limitava a progressão até 1, o que funcionava perfeitamente para esferas, e se quisesse o efeito a funcionar em esferas de tamanho 1, era ideal.
Mas por exemplo na estátua de cavalo do vídeo anterior com tamanho maior que 1 não iria funcionar.
Esta limitação era feita pelo node fraction que me devolvia sempre os valores decimais da progressão, logo nunca chegaria a maior que 1, e caso o valor da progressão continuasse a subir para além de 1, a onda iria repetir, removi o node fraction e aumentei o limite de progressão no script:
Com isto finalmente consegui a propagação da onda por toda a mesh:
Onda completa em qualquer mesh: Vídeo
O problema desta resolução, é que, caso um objeto seja maior que 5 vai dar problemas outra vez, mudei o máximo para 50 no script, assumindo que a maior parte dos objetos não precisariam de mais, mas é um valor alterável caso necessário, a partir do script.
Para terminar dei a opção ao utilizador de usar uma textura em vez do escudo e ruído feito anteriorimente, adicionando mais duas properidades, um booleano que indica se usamos textura ou não, e uma textura, este booleano é usado como predicado antes de serem dados os valores à Base Color, mas mantém as ondas:
Resultado final: Vídeo
Shader final:
Testei ainda com um cubo com mais polígonos para confirmar o problema que referi anteriormente, em que os cubos têm apenas 6 vértices e só deformava nos cantos, enquanto que este cubo tem mais e agora deformaria como deveria:
Cubo com mais vértices: Vídeo
Mudar o ponto default no script:
Enquanto adicionava novas meshs à demo scene, apercebi-me de um novo problema. O ponto default para diferentes meshs poderia ser diferente, no caso do plano, caso assumissemos que o focal point "nulo" é (0, 0, 0), isto acontecia:
Não arranjei solução correta para este problema, então alterei a posição default para um número bastante grande que assumi que muitos poucos modelos teriam, que, mais uma vez, caso fosse necessário, poderia ser alterado no script:
Demo scene final
Durante este trabalho ganhei bastante conhecimento sobre shaders, antes sentia que muitas coisas eram "magia" porque não percebia como funcionavam e também nao sabia sequer as possibilidade que um shader tem, por serem muitas, depois disto descobri muito do que se pode fazer, e apercebi-me quão extenso é.
Relembrei-me também que não devo tentar perceber as coisas sem as questionar, se tivesse questionado a normalização e o node fraction mais cedo teria poupado muita dor de cabeça.
Felizmente consegui obter o shader exatamente como queria, e ainda vejo que há muito espaço para expansão, como por exemplo adicionar a opção de textura no final, é possível adicionar muito mais coisas ainda, e penso continuar a trabalhar neste shader e em outros, pois acabei por gostar mais de shaders do que esperava.
Aprendi também que ao gravar os objetos como prefabs ao invés de os ter apenas na scene, reduz imenso o tamanho que a scene ocupa no ficheiro, pois ao invés de gravar os objetos na scene, grava-os como ficheiro, o que foi uma grande salvação quando a scene tinha mais de 100mb e já estava a usar o git lfs e o github não permitia mais que isto: Link para a discussão onde descobri isto
- David Brás: por me ajudar em alguns conceitos iniciais para o shader, especialmente a compreender melhor o Vertext Displacement;
- João Silva (a22004451): por me fornecer alguns modelos 3D feitos por ele com mais e menos polígonos para testar o shader.
- Shader Graph Forcefield: Update por Wilmer Lin GA School (17/07/2020)
- Energy Shield Effect in Unity URP Shader Graph por Imphenzia (20/04/2023)
- Shockwave Shader Graph - How to make a shock wave shader in Unity URP/HDRP por GameDevBill (08/12/2020)
- Unity Shader Graph VFX - Bubble Shield (Tutorial) por A Bit Of Game Dev (08/09/2021)
- Unity Shader Graph - Sci-Fi Barrier / Shield Tutorial por Gabriel Aguiar Prod. (06/09/2022)
- Unity Shader Graph - Intersection Effect Tutorial por Gabriel Aguiar Prod. (13/06/2023)
- 10 Shaders in 10 Minutes - Unity Shader Graph por Daniel Ilett (03/04/2022)
- Tree Model "Tree" (https://skfb.ly/onxWu) por Epic_Tree_Store está licenciado sob Creative Commons Atribuição (http://creativecommons.org/licenses/by/4.0/).
- Statue Model "Statue" (https://skfb.ly/opuMP) por DJMaesen está licenciado sob Creative Commons Atribuição (http://creativecommons.org/licenses/by/4.0/).



















































