Search Unity

  1. Megacity Metro Demo now available. Download now.
    Dismiss Notice
  2. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

Double-sided rendering without special shaders

Discussion in 'Shaders' started by Daniel_Brauer, Aug 28, 2013.

  1. Daniel_Brauer

    Daniel_Brauer

    Unity Technologies

    Joined:
    Aug 11, 2006
    Posts:
    3,355
    Double-sided rendering is an issue that seems to come up here a lot. For a detailed explanation of the problem and how to solve it, read this article.

    So the most sensible way by far of getting double-sided rendering is to actually double and flip the geometry in your model. This is always the most efficient and almost always the easiest way to solve the problem. If for whatever reason you can't or won't do this using a modelling program, you can use this AssetPostprocessor script I wrote.

    Just throw it into folder named Editor, and put the models you want doubled into a folder named Double-Sided. You might have to reimport your models to see the effects.

    Code (csharp):
    1. using UnityEngine;
    2. using UnityEditor;
    3. using System;
    4. using System.Collections.Generic;
    5.  
    6. // Doubles and flips imported geometry
    7.  
    8. public class DoubleSidedModelImporter : AssetPostprocessor {
    9.    
    10.     protected const string kFolderName = "double-sided";
    11.    
    12.     protected void OnPostprocessModel(GameObject modelPrefab) {
    13.        
    14.         // Only double geometry on models in appropriately named folders
    15.         if (!assetPath.ToLower().Contains(kFolderName)) {
    16.             return;
    17.         }
    18.        
    19.         // Collect all meshes, both static and skinned
    20.         var allMeshes = new HashSet<Mesh>();
    21.         var meshFilters = new List<MeshFilter>(
    22.             modelPrefab.GetComponentsInChildren<MeshFilter>()
    23.         );
    24.         foreach (var filter in meshFilters) {
    25.             if (!allMeshes.Contains(filter.sharedMesh)) {
    26.                 allMeshes.Add(filter.sharedMesh);
    27.             }
    28.         }
    29.         var skinnedMeshRenderers = new List<SkinnedMeshRenderer>(
    30.             modelPrefab.GetComponentsInChildren<SkinnedMeshRenderer>()
    31.         );
    32.         foreach (var skinnedRenderers in skinnedMeshRenderers) {
    33.             if (!allMeshes.Contains(skinnedRenderers.sharedMesh)) {
    34.                 allMeshes.Add(skinnedRenderers.sharedMesh);
    35.             }
    36.         }
    37.         foreach (var mesh in allMeshes) {
    38.            
    39.             // Invert normals on duplicated geometry
    40.             var oldVertexCount = mesh.vertexCount;
    41.             var newVertices = DoubleArray(mesh.vertices);
    42.             var newNormals = DoubleArray(mesh.normals);
    43.             for (var i = newNormals.Length/2; i < newNormals.Length; ++i) {
    44.                 newNormals[i] = -newNormals[i];
    45.             }
    46.            
    47.             // Invert tangent W components to account for mirrored UVs
    48.             var newTangents = DoubleArray(mesh.tangents);
    49.             for (var i = newTangents.Length/2; i < newTangents.Length; ++i) {
    50.                 newTangents[i].w = -newTangents[i].w;
    51.             }
    52.            
    53.             // All other attributes remain the same
    54.             var newColors = DoubleArray(mesh.colors);
    55.             var newUVs = DoubleArray(mesh.uv);
    56.             var newUV2s = DoubleArray(mesh.uv2);
    57.             var newBoneWeights = DoubleArray(mesh.boneWeights);
    58.            
    59.             // Reverse winding on doubled triangles so front face matches normal
    60.             // Also point doubled triangles at doubled vertex indices
    61.             var triangleLists = new List<int[]>();
    62.             for (var submeshIndex = 0; submeshIndex < mesh.subMeshCount; ++submeshIndex) {
    63.                 var oldTriangles = mesh.GetTriangles(submeshIndex);
    64.                 var newTriangles = DoubleArray(oldTriangles);
    65.                 for (var i = oldTriangles.Length/3; i < oldTriangles.Length/3*2; ++i) {
    66.                    
    67.                     newTriangles[i*3] += oldVertexCount;
    68.                    
    69.                     var temp = newTriangles[i*3+1] + oldVertexCount;
    70.                     newTriangles[i*3+1] = newTriangles[i*3+2] + oldVertexCount;
    71.                     newTriangles[i*3+2] = temp;
    72.                 }
    73.                 triangleLists.Add(newTriangles);
    74.             }
    75.            
    76.             // Assign all vertex attributes
    77.             mesh.vertices = newVertices;
    78.             mesh.normals = newNormals;
    79.             mesh.tangents = newTangents;
    80.             mesh.colors = newColors;
    81.             mesh.uv = newUVs;
    82.             mesh.uv2 = newUV2s;
    83.             mesh.boneWeights = newBoneWeights;
    84.            
    85.             // Assign triangles last, so they match vertices
    86.             for (var submeshIndex = 0; submeshIndex < mesh.subMeshCount; ++submeshIndex) {
    87.                 mesh.SetTriangles(triangleLists[submeshIndex], submeshIndex);
    88.             }
    89.         }
    90.     }
    91.    
    92.     // Returns the input array concatenated with itself
    93.     protected static T[] DoubleArray<T>(T[] input) {
    94.         var newArray = new T[input.Length*2];
    95.         Array.Copy(
    96.             input,
    97.             0,
    98.             newArray,
    99.             0,
    100.             input.Length
    101.         );
    102.         Array.Copy(
    103.             input,
    104.             0,
    105.             newArray,
    106.             input.Length,
    107.             input.Length
    108.         );
    109.         return newArray;
    110.     }
    111.    
    112. }
     
    NotaNaN and ina like this.
  2. eexo

    eexo

    Joined:
    Aug 14, 2013
    Posts:
    3
    well, maybe I'm stupid but what the problem with adding single line "Cull off" into shader code?
     
  3. mouurusai

    mouurusai

    Joined:
    Dec 2, 2011
    Posts:
    350
    Daniel Brauer, thank you, no more waiting while 3ds Max load to make just 2 clicks!
     
  4. Eric5h5

    Eric5h5

    Volunteer Moderator Moderator

    Joined:
    Jul 19, 2006
    Posts:
    32,401
    Click on the link to the article that Daniel provided in the first paragraph.

    --Eric
     
  5. aubergine

    aubergine

    Joined:
    Sep 12, 2009
    Posts:
    2,878
    This issue only pops up with planar objects(billboards, water..etc) and its not a good idea to double the vertex amount just to calculate inverse normals(for water kind of stuff with many vertices).
    I find it more optimised to get a sign of which side the camera is looking at using a plane equation and pass this value to the shader to invert the normals.
     
  6. Daniel_Brauer

    Daniel_Brauer

    Unity Technologies

    Joined:
    Aug 11, 2006
    Posts:
    3,355
    On what platforms have you tested this?
     
  7. aubergine

    aubergine

    Joined:
    Sep 12, 2009
    Posts:
    2,878
    Just windows and android
     
  8. Daniel_Brauer

    Daniel_Brauer

    Unity Technologies

    Joined:
    Aug 11, 2006
    Posts:
    3,355
    That's interesting. I'd like to see how the results compare with doubling in terms of runtime speed.

    In any case, vertex doubling is still easier to do, works without changing any shaders, and probably looks more correct in edge cases. I also wouldn't be surprised if it ran faster in some cases, although I admit I haven't tried your method or done any tests.
     
  9. aubergine

    aubergine

    Joined:
    Sep 12, 2009
    Posts:
    2,878
    With a fast Windows machine, it doesnt matter. On android, it is context sensitive to the amount of vertices you are dealing with.

    On a side note, doubling and flipping geometry on planar surfaces cause zfighting at odd situations depending on your offset.
     
  10. Daniel_Brauer

    Daniel_Brauer

    Unity Technologies

    Joined:
    Aug 11, 2006
    Posts:
    3,355
    Can you describe those situations? There shouldn't be any point where both the original and flipped triangle are shown at the same time.
     
  11. robitster

    robitster

    Joined:
    Apr 9, 2013
    Posts:
    5
    Tried this but when my character speaks you still see through the back of his head...
     
  12. jistyles

    jistyles

    Joined:
    Nov 6, 2013
    Posts:
    34
    That's a very broad generalisation which certainly does not hold true in all circumstances; it definitely CAN hold true in some cases, but in practice I've found it fails more often than not (especially when solely focusing on the performance aspect of the topic)

    In the last 6 years (around the x360/ps3/dx9 era), and even more so nowadays with modern hardware and rendering pipelines, it's much more common to be more performant to front-load the cost and trade off the interpolator/pixel side (this is even more compounded taking into account deferred pipelines usually being able to absorb any overhead in the cheaper gbuffer phase versus the multi-pass vertex impact of depth priming, shadow accumulation, seperate velocity passes, etc. All that extra vertex throughput adds up super quick nowadays!)

    To offer a few examples, this decision significantly impacts performance critical content relying on either dx9 OR dx11 style batching, vertex throughput as a bottleneck, procedural geometry creation, and in-flight bandwidth of interpolator heavy data.
    There's also the ever present non-trivial authoring complexity (eg normal projections, keeping animated parameters like vertex colours or manually edited normals perfectly synced up), etc.

    To share some anecdotal background here, the last time I did timings for comparison on this, PIX for x360 commonly showed a huge GPU win when using VFACE semantic for low density vegetation (dynamic vertex buffer style batching + lazy primitive collapsing also had good results for the draw thread too).
    For more recent hardware (and non-empirical/contextual data), razor was showing even larger GPU gains (assuming the combination of modern scalar hardware is much more adept at hiding the pixel cost, plus the fact that it now tidily handles the interpolater cost on unit, it's not wasting cycles there either).
     
  13. Sparrowfc

    Sparrowfc

    Joined:
    Jan 31, 2013
    Posts:
    100
    flip the mesh is a direct solution, but when it comes with physical cloth component, things won't work