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

Question Command Buffer Blit flipping render textures in scene view or game view

Discussion in 'General Graphics' started by rhedgeco, Oct 19, 2021.

  1. rhedgeco

    rhedgeco

    Joined:
    Dec 2, 2013
    Posts:
    19
    Hi All,
    I have been working on a project for generating pixel perfect sharp shadows. I am generating shadow volumes, and using a custom render feature in URP11 to inject the volumes into the _ScreenSpaceShadowmapTexture.

    It works swimmingly, and I am able to get it to render fine in the game view.
    The problem lies here:
    When I blit the texture into the existing _ScreenSpaceShadowmapTexture, I use a custom blit shader that flips the y coordinate like this.
    Code (CSharp):
    1. if (_ProjectionParams.x < 0)
    2.     o.texcoord.y = 1 - o.texcoord.y;
    This works out great in the game view, but it flips the texture in the scene view.
    I know this is not the biggest problem, but it is quite annoying, and I would like to fix it.

    Here is my render pass code for reference:
    Code (CSharp):
    1. using System.Collections.Generic;
    2. using UnityEngine;
    3. using UnityEngine.Rendering;
    4. using UnityEngine.Rendering.Universal;
    5.  
    6. namespace StencilShadowGenerator.Core.RenderFeature
    7. {
    8.     class ShadowVolumeRenderPass : ScriptableRenderPass
    9.     {
    10.         private readonly Material _occluderMaterial;
    11.         private readonly Material _shadowMaterial;
    12.         private readonly Material _blitMaterial;
    13.         private readonly ShaderTagId _volumeShader;
    14.         private readonly List<ShaderTagId> _occluderShaders;
    15.        
    16.         private RenderTargetIdentifier _shadowMap;
    17.         private RenderTargetHandle _tempTarget;
    18.         private ShadowVolumeRenderingSettings _settings;
    19.         private FilteringSettings _filteringSettings;
    20.  
    21.         public ShadowVolumeRenderPass(ShadowVolumeRenderingSettings settings)
    22.         {
    23.             _settings = settings;
    24.            
    25.             // set up render textures
    26.             _tempTarget = RenderTargetHandle.CameraTarget;
    27.             _shadowMap = new RenderTargetIdentifier(Shader.PropertyToID("_ScreenSpaceShadowmapTexture"));
    28.            
    29.             // set up materials
    30.             _occluderMaterial = new Material(Shader.Find("Hidden/ShadowVolumes/White"));
    31.             _shadowMaterial = new Material(Shader.Find("Hidden/ShadowVolumes/ShadowRender"));
    32.             _blitMaterial = new Material(Shader.Find("Hidden/ShadowVolumes/BlitFlip"));
    33.  
    34.             // set up shader tags
    35.             _volumeShader = new ShaderTagId("ShadowVolume");
    36.             _occluderShaders = new List<ShaderTagId>
    37.             {
    38.                 new ShaderTagId("UniversalForward"),
    39.                 new ShaderTagId("UniversalForwardOnly"),
    40.                 new ShaderTagId("LightweightForward"),
    41.                 new ShaderTagId("SRPDefaultUnlit")
    42.             };
    43.  
    44.             // set up render pass and filter settings
    45.             renderPassEvent = RenderPassEvent.BeforeRenderingOpaques;
    46.             _filteringSettings = new FilteringSettings(RenderQueueRange.opaque);
    47.         }
    48.  
    49.         public override void OnCameraSetup(CommandBuffer cmd, ref RenderingData renderingData)
    50.         {
    51.             RenderTextureDescriptor cameraTextureDescriptor = renderingData.cameraData.cameraTargetDescriptor;
    52.             cameraTextureDescriptor.depthBufferBits = 0;
    53.             cmd.GetTemporaryRT(_tempTarget.id, cameraTextureDescriptor, FilterMode.Point);
    54.             ConfigureTarget(_tempTarget.Identifier());
    55.         }
    56.  
    57.         public override void Configure(CommandBuffer cmd, RenderTextureDescriptor cameraTextureDescriptor)
    58.         {
    59.             ConfigureClear(ClearFlag.All, Color.white);
    60.         }
    61.  
    62.         public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
    63.         {
    64.             if (!_shadowMaterial) return;
    65.            
    66.             CommandBuffer cmd = CommandBufferPool.Get();
    67.             using (new ProfilingScope(cmd, new ProfilingSampler("Shadow Volume Rendering")))
    68.             {
    69.                 // prepare and clear buffer
    70.                 context.ExecuteCommandBuffer(cmd);
    71.                 cmd.Clear();
    72.                
    73.                 // set matrices
    74.                 Camera camera = renderingData.cameraData.camera;
    75.                 cmd.SetViewProjectionMatrices(camera.worldToCameraMatrix, camera.projectionMatrix);
    76.                
    77.                 // draw occluders
    78.                 DrawingSettings occluderSettings = CreateDrawingSettings(_occluderShaders,
    79.                     ref renderingData, SortingCriteria.CommonOpaque);
    80.                 occluderSettings.overrideMaterial = _occluderMaterial;
    81.                 context.DrawRenderers(renderingData.cullResults, ref occluderSettings, ref _filteringSettings);
    82.                
    83.                 // draw shadow volume stencil
    84.                 DrawingSettings volumeSettings = CreateDrawingSettings(_volumeShader,
    85.                     ref renderingData, SortingCriteria.CommonOpaque);
    86.                 context.DrawRenderers(renderingData.cullResults, ref volumeSettings, ref _filteringSettings);
    87.                
    88.                 // draw shadow material using fullscreen quad
    89.                 cmd.SetViewProjectionMatrices(Matrix4x4.identity, Matrix4x4.identity);
    90.                 cmd.DrawMesh(RenderingUtils.fullscreenMesh, Matrix4x4.identity, _shadowMaterial);
    91.                 cmd.SetViewProjectionMatrices(camera.worldToCameraMatrix, camera.projectionMatrix);
    92.                
    93.                 // blit to shadow texture
    94.                 cmd.Blit(_tempTarget.Identifier(), _shadowMap, _blitMaterial);
    95.             }
    96.  
    97.             context.ExecuteCommandBuffer(cmd);
    98.             CommandBufferPool.Release(cmd);
    99.         }
    100.  
    101.         public override void OnCameraCleanup(CommandBuffer cmd)
    102.         {
    103.             cmd.ReleaseTemporaryRT(_tempTarget.id);
    104.         }
    105.     }
    106. }
    Here is an example of the differences. Left is scene view, right is game view.
    upload_2021-10-19_10-33-59.png
     
  2. rhedgeco

    rhedgeco

    Joined:
    Dec 2, 2013
    Posts:
    19
  3. burningmime

    burningmime

    Joined:
    Jan 25, 2014
    Posts:
    845
    There's a #define for it.

    Code (CSharp):
    1. #if UNITY_UV_STARTS_AT_TOP
    2. o.texcoord.y = 1 - o.texcoord.y;
    3. #endif
     
  4. rhedgeco

    rhedgeco

    Joined:
    Dec 2, 2013
    Posts:
    19
    Here is the result when using that define tag instead of my flip code.
    It seems to have the same exact result :(
    upload_2021-10-24_22-31-36.png
     
  5. burningmime

    burningmime

    Joined:
    Jan 25, 2014
    Posts:
    845
    Another thing you can try is because you're in a render pass, you can call
    Blit(cmd, ...)
    instead of
    cmd.Blit(...)
    . This will cause it to call
    ScriptableRenderer.SetRenderTarget
    before the
    cmd.Blit()
    .

    If that doesn't work, I'd just hack it. Add a shader constant for "_EditorYFlip" or something and set that if !Application.isPlaying (should have almost zero runtime overhead since the driver will compile it to a preshader, but if you want to be extra sure, use a macro define and shader variant instead).
     
  6. rhedgeco

    rhedgeco

    Joined:
    Dec 2, 2013
    Posts:
    19
    Hmmm, that's unfortunate.
    the Blit() method has the same result. Its definitely strange that it works opposite in editor and game view.
    Does this potentially change depending on the platform that its built for? I sure hope not lol.

    Also if I were to manually change the flip settings when running, it will just be flipped in the scene view while the game is running because Application.isPlaying is true even in scene view. This is not really optimal. I am making this as a tool for others and would definitely prefer to not have such a strange effect.

    It seems a bit awkward that this is not something that is well documented or accounted for. Is there a way for me to tag/ask a unity dev about this? I might also just file a bug report.
     
  7. burningmime

    burningmime

    Joined:
    Jan 25, 2014
    Posts:
    845
    There's
    camera.cameraType == CameraType.SceneView
    . But that shouldn't be needed; it should just work the same for all camera types.

    Yup; that sounds like the right thing to do.

    Out of curiosity, what are you doing for edge extrusion? My unfinished stencil shadow feature is doing that part in a compute shader. The one on the asset store already is just rendering every edge as a quad and letting the vertex shader make the non-light-facing edges degenerate (which breaks on some skinned meshes, but works on mobile and older hardware).
     
  8. rhedgeco

    rhedgeco

    Joined:
    Dec 2, 2013
    Posts:
    19
    upload_2021-10-25_16-50-30.png
    ahhhh what a beautiful thing. cool looking shadows with ugly looking code lol

    I have had a few approaches. The most optimal seems to be similar to the one on the asset store. I was using the job system/burst compiler for a bit, but I removed that for now. If you look at the commit history you might find it lol.
    https://github.com/rhedgeco/UnityShadowVolumeGenerator

    Currently I do this incredibly inefficient nested loop to match edges up on a mesh.
    I first split each triangle so that no vertices are shared, and then I generate quads in between all the edges. It definitely costs more in memory (its actually not too bad), but its SUPER cheap at runtime when all I have to do is displace vertices that face away from the light source using the vertex shader.

    Edit:
    What im REALLY waiting for is the ability to batch draw calls using cmd.drawmesh so that I can draw each shadow volume in a loop by just accessing the generated shadow mesh. Currently it has to be an object that exists in the scene with a renderer component so that I can use context.DrawrRenderers().
     

    Attached Files:

  9. burningmime

    burningmime

    Joined:
    Jan 25, 2014
    Posts:
    845
    That rendering method works fine for static meshes, but can break if you have skinned meshes with skinny arms and legs (like me). The problem is that per-vertex normals aren't perfectly accurate anymore; you actually need the normals of triangles on either side of the edge which depend on 4 different vertex positions. That's what eventually got me to do it in a compute shader (old games did it all on the CPU, but obviously that's not scalable).

    But as long as you don't have skinned meshes (or all your skinned meshes are 2005-Unreal-Engine levels of swoll), the vertex shader method works, and lets you take adavantage of all the batching/culling in the render pipelines already. And it looks like your method is targeted at mobile/etc, so compute shaders might not be available or fast enough.

    Edge matching can be sped up with a hash or by sorting them (Burst NativeMultiHashMap<> is kinda terrible, so I ended up using the latter): https://gitlab.com/burningmime/urpg/-/blob/master/Packages/shadow/src/internal/FindEdgesJob.cs

    It's fun seeing different approaches to this, and it's cool you took the time to write yours up as an actual tutorial. Although it actually can be used for soft shadows by using a screen-space blur that takes depth/normal of center pixel into account. Performance is not terrible, especially when compared to techniques where you do multiple PCF samples, but of course you won't get contact hardening.
     
    Last edited: Oct 27, 2021
  10. rhedgeco

    rhedgeco

    Joined:
    Dec 2, 2013
    Posts:
    19
    I appreciate the support and tips!
    And yeah as it stands it REALLY doesn't support skinned meshes considering I just create a renderer with a custom created mesh at runtime. Ill probably change that soon tho lol.

    Maybe it could still work with skinned meshes. If the problem is that normals get messed up during deformation, couldn't you just run through and just apply a new normal calculation? Then you can skip the conversion to an entirely different shadow volume generation method. I have no idea though since I haven't got there yet.

    In my mind you already have access to the triangle list, and the order of the vertices determines face direction. So you could just do a cross product with the vertices and calculate perfect face normals for each one right?
     
  11. burningmime

    burningmime

    Joined:
    Jan 25, 2014
    Posts:
    845
    Exactly! But you need to do that every frame, based on the skinned positions of the triangles.
     
  12. rhedgeco

    rhedgeco

    Joined:
    Dec 2, 2013
    Posts:
    19
    Well I guess I'll investigate the speed of reprocessing normals every frame, and we will have a speed competition ;)