Ma dernière tâche en graphisme / rendu technique a été de trouver une bonne solution pour le rendu de l'eau. En particulier, le rendu de jets d'eau minces et rapides à base de particules. Au cours de la semaine dernière, j'ai pensé à de bons résultats, je vais donc écrire un article à ce sujet.
Je n'aime pas vraiment l'approche des cubes voxélisés / marchants lors du rendu de l'eau (voir, par exemple, le rendu d'une simulation fluide dans Blender). Lorsque le volume d'eau est à la même échelle que la grille utilisée pour le rendu, le mouvement est sensiblement discret. Ce problème peut être résolu en augmentant la résolution de la grille, mais pour les jets minces sur des distances relativement longues en temps réel, il est tout simplement peu pratique car il affecte considérablement le temps d'exécution et la mémoire occupée. (Il existe un précédent pour l'utilisation de structures de voxels clairsemées pour améliorer la situation. Mais je ne sais pas dans quelle mesure cela fonctionne pour les systèmes dynamiques. De plus, ce n'est pas le niveau de complexité avec lequel je voudrais travailler.)
La première alternative que j'ai explorée était les Screen Space Meshes de Müller. Ils utilisent le rendu des particules d'eau dans un tampon de profondeur, le lissant, reconnaissant les fragments connectés de profondeur similaire et construisant un maillage à partir du résultat à l'aide de carrés de marche. Aujourd'hui, cette méthode est probablement devenue
plus applicable qu'en 2007 (puisque maintenant nous pouvons créer un maillage dans le shader de calcul), mais elle est toujours associée à un niveau de complexité et de coût plus élevé que je ne le souhaiterais.
Au final, j'ai trouvé la présentation de Simon Green avec GDC 2010, Screen Space Fluid Rendering For Games. Il commence exactement de la même manière que les maillages d'espace d'écran: avec le rendu des particules dans le tampon de profondeur et le lissage. Mais au lieu de construire le maillage, le tampon résultant est utilisé pour ombrer et composer le liquide dans la scène principale (en enregistrant explicitement la profondeur.) J'ai décidé de mettre en œuvre un tel système.
La préparation
Plusieurs projets Unity précédents m'ont appris à ne pas gérer les limites du rendu du moteur. Par conséquent, les tampons fluides sont rendus par une deuxième caméra avec une profondeur de champ plus faible de sorte qu'elle effectue un rendu devant la scène principale. Chaque système fluide existe sur une couche de rendu distincte; la chambre principale exclut une couche d'eau, et la deuxième chambre ne rend que de l'eau. Les deux caméras sont les enfants d'un objet vide pour assurer leur orientation relative.
Un tel schéma signifie que je peux rendre presque n'importe quoi dans la couche liquide, et il ressemblera à ce que j'attends. Dans le contexte de ma scène de démonstration, cela signifie que quelques jets et éclaboussures de sous-émetteurs peuvent fusionner. De plus, cela permettra le mélange d'autres systèmes d'eau, par exemple, des volumes basés sur des champs d'altitude, qui peuvent ensuite être rendus les mêmes. (Je n'ai pas encore testé cela.)
La source d'eau dans ma scène est un système de particules standard. En fait, aucune simulation de fluide n'est effectuée. Ceci, à son tour, signifie que les particules ne se chevauchent pas de manière complètement physique, mais le résultat final semble acceptable dans la pratique.
Rendu de tampon fluide
La première étape de cette technique consiste à rendre le tampon fluide de base. Il s'agit d'un tampon hors écran qui contient (au stade actuel de mon implémentation) les éléments suivants: largeur de fluide, vecteur de mouvement dans l'espace d'écran et valeur de bruit. De plus, nous rendons le tampon de profondeur en enregistrant explicitement la profondeur du fragment shader afin de transformer chaque quadrilatère d'une particule en une "boule" sphérique (enfin elliptique).
Les calculs de profondeur et de largeur sont assez simples:
frag_out o; float3 N; N.xy = i.uv*2.0 - 1.0; float r2 = dot(N.xy, N.xy); if (r2 > 1.0) discard; Nz = sqrt(1.0 - r2); float4 pixel_pos = float4(i.view_pos + N * i.size, 1.0); float4 clip_pos = mul(UNITY_MATRIX_P, pixel_pos); float depth = clip_pos.z / clip_pos.w; o.depth = depth; float thick = Nz * i.size * 2;
(Bien sûr, les calculs de profondeur peuvent être simplifiés; à partir de la position du clip, nous n'avons besoin que de z et w.)
Un peu plus tard, nous reviendrons sur le fragment shader pour les vecteurs mouvement et bruit.
Le plaisir commence dans le vertex shader, et c'est ici que je m'éloigne de la technique Green. Le but de ce projet est de rendre des jets d'eau à grande vitesse; il peut être réalisé à l'aide de particules sphériques, mais une énorme quantité d'entre elles sera nécessaire pour créer un jet continu. Au lieu de cela, j'étirerai les quadrangles des particules en fonction de leur vitesse, qui à son tour étire les boules de profondeur, les rendant non sphériques, mais elliptiques. (Étant donné que les calculs de profondeur sont basés sur les UV, qui ne changent pas, tout fonctionne simplement.)
Les utilisateurs expérimentés d'Unity peuvent se demander pourquoi je n'utilise tout simplement pas le mode de panneau d'affichage étendu intégré disponible dans le système de particules Unity. Le panneau d'affichage étiré effectue un étirement inconditionnel le long du vecteur vitesse dans l'espace du monde. Dans le cas général, cela convient parfaitement, mais cela conduit à un problème très notable lorsque le vecteur vitesse est codirigé avec le vecteur caméra orienté vers l'avant (ou très proche de celui-ci). Le panneau d'affichage s'étire sur l'écran, ce qui rend sa nature bidimensionnelle très visible.
Au lieu de cela, j'utilise un panneau d'affichage visant la caméra et projette le vecteur vitesse sur le plan de la particule, en l'utilisant pour étirer le quadrilatère. Si le vecteur vitesse est perpendiculaire au plan (dirigé vers l'écran ou éloigné de lui), alors la particule reste non étirée et sphérique, comme il se doit, et lorsqu'elle est inclinée, la particule est étirée dans cette direction, ce dont nous avons besoin.
Laissons une longue explication, voici une fonction assez simple:
float3 ComputeStretchedVertex(float3 p_world, float3 c_world, float3 vdir_world, float stretch_amount) { float3 center_offset = p_world - c_world; float3 stretch_offset = dot(center_offset, vdir_world) * vdir_world; return p_world + stretch_offset * lerp(0.25f, 3.0f, stretch_amount); }
Pour calculer le vecteur de mouvement de l'espace d'écran, nous calculons deux ensembles de positions de vecteurs:
float3 vp1 = ComputeStretchedVertex( vertex_wp, center_wp, velocity_dir_w, rand); float3 vp0 = ComputeStretchedVertex( vertex_wp - velocity_w * unity_DeltaTime.x, center_wp - velocity_w * unity_DeltaTime.x, velocity_dir_w, rand); o.motion_0 = mul(_LastVP, float4(vp0, 1.0)); o.motion_1 = mul(_CurrVP, float4(vp1, 1.0));
Notez que puisque nous calculons des vecteurs de mouvement dans le passage principal et non dans le passage de vecteurs de vitesse, Unity ne nous fournit pas une projection de courant précédente ou non déformée à partir de la vue. Pour résoudre ce problème, j'ai ajouté un script simple aux systèmes de particules correspondants:
public class ScreenspaceLiquidRenderer : MonoBehaviour { public Camera LiquidCamera; private ParticleSystemRenderer m_ParticleRenderer; private bool m_First; private Matrix4x4 m_PreviousVP; void Start() { m_ParticleRenderer = GetComponent(); m_First = true; } void OnWillRenderObject() { Matrix4x4 current_vp = LiquidCamera.nonJitteredProjectionMatrix * LiquidCamera.worldToCameraMatrix; if (m_First) { m_PreviousVP = current_vp; m_First = false; } m_ParticleRenderer.material.SetMatrix("_LastVP", GL.GetGPUProjectionMatrix(m_PreviousVP, true)); m_ParticleRenderer.material.SetMatrix("_CurrVP", GL.GetGPUProjectionMatrix(current_vp, true)); m_PreviousVP = current_vp; } }
Je mets en cache la matrice précédente manuellement car Camera.previousViewProjectionMatrix donne des résultats incorrects.
¯ \ _ (ツ) _ / ¯
(De plus, cette méthode viole le rendu; il peut être prudent de définir des constantes matricielles globales dans la pratique plutôt que de les utiliser pour chaque matériau.)
Revenons au shader de fragment: nous utilisons les positions projetées pour calculer les vecteurs de mouvement de l'espace écran:
float3 hp0 = i.motion_0.xyz / i.motion_0.w; float3 hp1 = i.motion_1.xyz / i.motion_1.w; float2 vp0 = (hp0.xy + 1) / 2; float2 vp1 = (hp1.xy + 1) / 2; #if UNITY_UV_STARTS_AT_TOP vp0.y = 1.0 - vp0.y; vp1.y = 1.0 - vp1.y; #endif float2 vel = vp1 - vp0;
(Le calcul des vecteurs de mouvement est pratiquement inchangé, extrait de
https://github.com/keijiro/ParticleMotionVector/blob/master/Assets/ParticleMotionVector/Shaders/Motion.cginc )
Enfin, la dernière valeur dans le tampon fluide est le bruit. J'utilise un nombre aléatoire stable pour chaque particule pour sélectionner l'un des quatre bruits (regroupés dans une seule texture). Ensuite, il est mis à l'échelle par la vitesse et l'unité moins la taille des particules (par conséquent, les particules rapides et petites sont plus bruyantes). Cette valeur de bruit est utilisée dans la passe d'ombrage pour déformer les normales et ajouter une couche de mousse. Le travail de Green utilise un bruit blanc à trois canaux, mais un travail plus récent (Rendu fluide de l'espace spatial avec flux de courbure) propose d'utiliser le bruit Perlin. J'utilise le bruit Voronoi / bruit cellulaire avec différentes échelles:
Problèmes de mixage (et solutions)
Et voici les premiers problèmes de ma mise en œuvre apparaissent. Pour le calcul correct de l'épaisseur des particules sont mélangées additivement. Étant donné que le mixage affecte toutes les sorties, cela signifie que les vecteurs de bruit et de mouvement sont également mélangés de manière additive. Le bruit additif nous convient tout à fait, mais pas les vecteurs additifs, et si vous les laissez tels quels, vous obtenez un anti-aliasing de temps dégoûtant (TAA) et un flou de mouvement. Pour résoudre ce problème, lors du rendu d'un tampon fluide, je multiplie simplement les vecteurs de mouvement par l'épaisseur et divise par l'épaisseur totale dans la passe d'ombrage. Cela nous donne un vecteur de mouvement moyen pondéré pour toutes les particules qui se chevauchent; pas tout à fait ce dont nous avons besoin (des artefacts étranges sont créés lorsque plusieurs jets se croisent), mais tout à fait acceptable.
Un problème plus complexe est la profondeur; Pour un rendu correct du tampon de profondeur, nous devons avoir à la fois l'enregistrement et la vérification de la profondeur actifs. Cela peut entraîner des problèmes si les particules ne sont pas triées (car la différence dans l'ordre de rendu peut entraîner l'écrêtage de la sortie des particules superposées par d'autres). Par conséquent, nous ordonnons au système de particules Unity de trier les particules par profondeur, puis nous croisons les doigts et espérons. que les systèmes rendront également en profondeur. Nous aurons * des * cas de systèmes qui se chevauchent (par exemple, l'intersection de deux jets de particules) qui ne sont pas traités correctement, ce qui conduira à une épaisseur plus petite. Mais cela ne se produit pas très souvent et n'affecte pas beaucoup l'apparence.
Très probablement, la bonne approche serait de séparer complètement les tampons de profondeur et de couleur; le retour sur investissement sera un rendu en deux passes. Il convient d'explorer ce problème lors de la configuration du système.
Lissage en profondeur
Enfin, la chose la plus importante dans la technique verte. Nous avons rendu un tas de boules sphériques dans le tampon de profondeur, mais en réalité, l'eau n'est pas constituée de «boules». Alors maintenant, nous prenons cette approximation et la rendons floue pour qu'elle ressemble davantage à la surface d'un liquide.
L'approche naïve consiste simplement à appliquer des profondeurs de bruit gaussiennes à l'ensemble du tampon. Cela crée des résultats étranges - il lisse les points éloignés plus que les points proches et brouille les bords des silhouettes. Au lieu de cela, nous pouvons modifier le rayon de flou en profondeur et utiliser un flou bilatéral pour enregistrer les bords.
Un seul problème se pose ici: de tels changements rendent le flou indivisible. Le flou partagé peut être effectué en deux passes: flou horizontalement, puis verticalement. Le flou indiscernable se fait en un seul passage. Cette différence est importante car le flou partagé est mis à l'échelle de façon linéaire (O (w) + O (h)) et le flou non partagé est mis à l'échelle de façon nette (O (w * h)). Le flou à grande échelle et non partagé devient rapidement inapplicable dans la pratique.
En tant qu'adultes, développeurs responsables, nous pouvons faire le pas évident: fermer les yeux, prétendre que le bruit bidirectionnel * est * partagé, et toujours l'implémenter avec des allées horizontales et verticales séparées.
Green dans sa présentation a démontré que bien que cette approche
crée des artefacts dans le résultat résultant (en particulier lors de la reconstruction de normales), la phase d'ombrage les masque bien. Lorsque vous travaillez avec les jets d'eau plus étroits que je crée, ces artefacts sont encore moins visibles et n'affectent pas particulièrement le résultat.
Ombrage
Nous avons finalement fini de travailler avec le tampon fluide. Passons maintenant à la deuxième partie de l'effet: l'ombrage et la composition de l'image principale.
Nous rencontrons ici de nombreuses restrictions de rendu Unity. J'ai décidé d'éclairer l'eau uniquement avec la lumière du soleil et de la skybox; La prise en charge de sources d'éclairage supplémentaires nécessite soit plusieurs passes (c'est du gaspillage!) Soit la construction d'une structure de recherche d'éclairage côté GPU (coûteuse et plutôt compliquée). De plus, comme Unity ne donne pas accès aux plans d'ombres et que les lumières directionnelles utilisent des ombres d'espace d'écran (basées sur un tampon de profondeur rendu par une géométrie opaque), nous n'avons pas accès aux informations sur les ombres provenant d'une source de lumière solaire. Vous pouvez attacher un tampon de commande à une source de lumière du soleil pour créer une carte d'ombre de l'espace d'écran spécifiquement pour l'eau, mais jusqu'à présent je ne l'ai pas fait.
La dernière étape de l'ombrage est contrôlée par un script et utilise le tampon de commande pour envoyer des appels de tirage. Cela est
nécessaire car la texture du vecteur de mouvement (utilisée pour l'anti-aliasing temporaire (TAA) et le flou de mouvement) ne peut pas être utilisée pour le rendu direct à l'aide de Graphics.SetRenderTarget (). Dans le script joint à la caméra principale, nous écrivons ce qui suit:
void Start() {
Les tampons de couleur et les vecteurs de mouvement ne peuvent pas être rendus simultanément avec MRT (cibles de rendu multiples). Je n'ai pas pu trouver la raison. De plus, ils nécessitent une liaison à des tampons de profondeur différents. Heureusement, nous écrivons la profondeur dans
ces deux tampons de profondeur, donc la re-projection de l'anti-aliasing temporaire fonctionne très bien (oh, c'est un plaisir de travailler avec le moteur de boîte noire).
Dans chaque image, nous lançons un rendu composite de OnPostRender ():
RenderTexture GenerateRefractionTexture() { RenderTexture result = RenderTexture.GetTemporary(m_MainCamera.activeTexture.descriptor); Graphics.Blit(m_MainCamera.activeTexture, result); return result; } void OnPostRender() { if (ScreenspaceLiquidCamera && ScreenspaceLiquidCamera.IsReady()) { RenderTexture refraction_texture = GenerateRefractionTexture(); m_Mat.SetTexture("_MainTex", ScreenspaceLiquidCamera.GetColorBuffer()); m_Mat.SetVector("_MainTex_TexelSize", ScreenspaceLiquidCamera.GetTexelSize()); m_Mat.SetTexture("_LiquidRefractTexture", refraction_texture); m_Mat.SetTexture("_MainDepth", ScreenspaceLiquidCamera.GetDepthBuffer()); m_Mat.SetMatrix("_DepthViewFromClip", ScreenspaceLiquidCamera.GetProjection().inverse); if (SunLight) { m_Mat.SetVector("_SunDir", transform.InverseTransformVector(-SunLight.transform.forward)); m_Mat.SetColor("_SunColor", SunLight.color * SunLight.intensity); } else { m_Mat.SetVector("_SunDir", transform.InverseTransformVector(new Vector3(0, 1, 0))); m_Mat.SetColor("_SunColor", Color.white); } m_Mat.SetTexture("_ReflectionProbe", ReflectionProbe.defaultTexture); m_Mat.SetVector("_ReflectionProbe_HDR", ReflectionProbe.defaultTextureHDRDecodeValues); Graphics.ExecuteCommandBuffer(m_CommandBuffer); RenderTexture.ReleaseTemporary(refraction_texture); } }
Et c'est là que la participation du processeur se termine, plus tard seuls les shaders vont.
Commençons par le passage des vecteurs de mouvement. Voici à quoi ressemble le shader:
#include "UnityCG.cginc" sampler2D _MainDepth; sampler2D _MainTex; struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; }; struct v2f { float2 uv : TEXCOORD0; float4 vertex : SV_POSITION; }; v2f vert(appdata v) { v2f o; o.vertex = mul(UNITY_MATRIX_P, v.vertex); o.uv = v.uv; return o; } struct frag_out { float4 color : SV_Target; float depth : SV_Depth; }; frag_out frag(v2f i) { frag_out o; float4 fluid = tex2D(_MainTex, i.uv); if (fluid.a == 0) discard; o.depth = tex2D(_MainDepth, i.uv).r; float2 vel = fluid.gb / fluid.a; o.color = float4(vel, 0, 1); return o; }
La vitesse dans l'espace de l'écran est stockée dans les canaux vert et bleu du tampon fluide. Puisque nous avons mis la vitesse à l'échelle en fonction de l'épaisseur lors du rendu du tampon, nous divisons à nouveau l'épaisseur totale (située dans le canal alpha) pour obtenir une vitesse moyenne pondérée.
Il convient de noter que lorsque vous travaillez avec de grands volumes d'eau, une autre méthode de traitement du tampon de vitesse peut être nécessaire. Puisque nous effectuons un rendu sans mélange, les vecteurs de mouvement pour tout ce qui se trouve
derrière l' eau sont perdus, détruisant le TAA et le flou de mouvement de ces objets. Lorsque vous travaillez avec de minces jets d'eau, ce n'est pas un problème, mais cela peut interférer lorsque vous travaillez avec une piscine ou un lac lorsque nous avons besoin de TAA ou d'objets flou de mouvement pour être clairement visibles à travers la surface.
Le passage d'ombrage principal est plus intéressant. Notre première priorité après masquage avec l'épaisseur du liquide est de reconstruire la position et la normale de l'espace de visualisation (espace de visualisation).
float3 ViewPosition(float2 uv) { float clip_z = tex2D(_MainDepth, uv).r; float clip_x = uv.x * 2.0 - 1.0; float clip_y = 1.0 - uv.y * 2.0; float4 clip_p = float4(clip_x, clip_y, clip_z, 1.0); float4 view_p = mul(_DepthViewFromClip, clip_p); return (view_p.xyz / view_p.w); } float3 ReconstructNormal(float2 uv, float3 vp11) { float3 vp12 = ViewPosition(uv + _MainTex_TexelSize.xy * float2(0, 1)); float3 vp10 = ViewPosition(uv + _MainTex_TexelSize.xy * float2(0, -1)); float3 vp21 = ViewPosition(uv + _MainTex_TexelSize.xy * float2(1, 0)); float3 vp01 = ViewPosition(uv + _MainTex_TexelSize.xy * float2(-1, 0)); float3 dvpdx0 = vp11 - vp12; float3 dvpdx1 = vp10 - vp11; float3 dvpdy0 = vp11 - vp21; float3 dvpdy1 = vp01 - vp11;
C'est une façon coûteuse de reconstruire la position de l'espace de visualisation: on prend la position dans l'espace clip et on effectue l'opération inverse de projection.
Après avoir trouvé un moyen de reconstruire les positions, les normales sont plus simples: nous calculons la position des points voisins dans le tampon de profondeur et construisons une base tangente à partir d'eux. Pour travailler avec les bords des silhouettes, nous échantillonnons dans les deux directions et sélectionnons le point le plus proche de l'espace de vue pour reconstruire la normale. Cette méthode fonctionne étonnamment bien et ne pose de problèmes que dans le cas d'objets très minces.
Cela signifie que nous effectuons cinq opérations de projection inverse distinctes par pixel (pour le point actuel et quatre voisines). Il existe un moyen moins cher, mais ce post est déjà trop long, donc je le laisse pour plus tard.
Les normales résultantes sont:
Je déforme cette normale calculée en utilisant les dérivées de la valeur de bruit du tampon fluide, mise à l'échelle par le paramètre de force et normalisée en divisant par l'épaisseur du jet (pour la même raison que pour la vitesse):
N.xy += NoiseDerivatives(i.uv, fluid.r) * (_NoiseStrength / fluid.a); N = normalize(N);
Nous pouvons enfin procéder à l'ombrage lui-même. L'ombrage de l'eau se compose de trois parties principales: la réflexion spéculaire, la réfraction spéculaire et la mousse.
La réflexion est un GGX standard entièrement repris du shader Unity standard. (Avec une correction, le F0 correct de 2% est utilisé pour l'eau.)
Avec la réfraction, tout est plus intéressant. Une réfraction correcte nécessite un lancer de rayons (ou un raymarching pour un résultat approximatif). Heureusement, la réfraction est moins intuitive à l'œil que la réflexion, et donc des résultats incorrects ne sont pas si visibles. Par conséquent, nous décalons l'échantillon UV pour la texture réfractive par les normales x et y, mises à l'échelle par le paramètre d'épaisseur et de force:
float aspect = _MainTex_TexelSize.y * _MainTex_TexelSize.z; float2 refract_uv = (i.grab_pos.xy + N.xy * float2(1, -aspect) * fluid.a * _RefractionMultiplier) / i.grab_pos.w; float4 refract_color = tex2D(_LiquidRefractTexture, refract_uv);
(Notez que la correction de corrélation est utilisée; elle est
facultative - après tout, ce n'est qu'une approximation, mais l'ajouter est assez simple.)
Cette lumière réfractée passe à travers le liquide, donc une partie est absorbée:
float3 water_color = _AbsorptionColor.rgb * _AbsorptionIntensity; refract_color.rgb *= exp(-water_color * fluid.a);
Notez que _AbsorptionColor est déterminé exactement à l'opposé de la manière attendue: les valeurs de chaque canal indiquent la quantité de lumière
absorbée plutôt que transmise. Par conséquent, _AbsorptionColor avec une valeur de (1, 0, 0) ne donne pas le rouge, mais une couleur turquoise (turquoise).
La réflexion et la réfraction sont mélangées à l'aide de coefficients de Fresnel:
float spec_blend = lerp(0.02, 1.0, pow(1.0 - ldoth, 5)); float4 clear_color = lerp(refract_color, spec, spec_blend);
Jusqu'à ce moment, nous avons respecté les règles (principalement) et utilisé un ombrage physique.
Il est assez bon, mais il a un problème avec l'eau. C'est un peu difficile à voir:
Pour le réparer, ajoutons un peu de mousse.
La mousse apparaît lorsque l'eau est turbulente et l'air se mélange à l'eau pour former des bulles. Ces bulles créent toutes sortes de variations de réflexion et de réfraction, ce qui donne à toute l'eau une sensation d'éclairage diffus. Je modéliserai ce comportement avec de la lumière ambiante enveloppée:
float3 foam_color = _SunColor * saturate((dot(N, L)*0.25f + 0.25f));
Il est ajouté à la couleur finale à l'aide d'un facteur spécial, en fonction du bruit du fluide et du coefficient de Fresnel ramolli:
float foam_blend = saturate(fluid.r * _NoiseStrength) * lerp(0.05f, 0.5f, pow(1.0f - ndotv, 3)); clear_color.rgb += foam_color * saturate(foam_blend);
L'éclairage ambiant enveloppé est normalisé pour conserver l'énergie afin qu'il puisse être utilisé comme approximation de la diffusion. Le mélange de la couleur de la mousse est plus visible. Il s'agit d'une violation assez claire de la loi de conservation de l'énergie.
Mais en général, tout semble bon et rend le flux plus visible:
Poursuite des travaux et améliorations
Dans le système créé, beaucoup peut être amélioré.
- Utilisation de plusieurs couleurs. À l'heure actuelle, l'absorption est calculée uniquement au dernier stade de l'ombrage et utilise une couleur et une luminosité constantes pour tout le liquide sur l'écran. La prise en charge de différentes couleurs est possible, mais nécessite un deuxième tampon de couleur et la solution de l'intégrale d'absorption pour chaque particule dans le processus de rendu du tampon fluide de base. Cela pourrait potentiellement être une opération coûteuse.
- Couverture complète. Ayant accès à la structure de recherche d'éclairage côté GPU (soit à la main, soit grâce à la liaison avec le nouveau pipeline de rendu Unity HD), nous pouvons éclairer correctement l'eau avec un nombre illimité de sources de lumière et créer le bon éclairage ambiant.
- Amélioration de la réfraction. Avec les textures floues de la texture d'arrière-plan, nous pouvons mieux simuler la réfraction pour les surfaces rugueuses. En pratique, cela n'est pas très utile pour de petites pulvérisations de liquide, mais peut être utile pour des volumes plus importants.
Si j'en avais l'occasion, j'améliorerais ce système à la perte d'une impulsion, mais pour le moment on peut la qualifier de complète.