Search Unity

  1. Unity 6 Preview is now available. To find out what's new, have a look at our Unity 6 Preview blog post.
    Dismiss Notice
  2. Unity is excited to announce that we will be collaborating with TheXPlace for a summer game jam from June 13 - June 19. Learn more.
    Dismiss Notice

Getting a pixel look in 3D

Discussion in 'General Graphics' started by Hi_ImTemmy, Feb 6, 2019.

  1. Hi_ImTemmy

    Hi_ImTemmy

    Joined:
    Jul 8, 2015
    Posts:
    174
    Hey folks!

    I'm working on a 3D game where I want to give it a nice pixely look.

    Here's what it looks like so far




    To achieve this, I'm currently rendering to a render texture and then filling the screen with that render texture. This gives everything a nice, blocky aliased look and everything is still affected by lighting. Perfect. I love it.

    The problem I have though is this approach is a blanket approach. Everything in the game world is affected by this. I can see myself maybe wanting better control over some things in the world (like 3D text objects which I plan to add) to be untouched by this effect to help legibility.

    So, how can I do this more smartly? Is there a way I can do this on a per-object basis but still achieve the overall look above? What would you suggest?

    Thanks for your help!
     
    insomnisan and Lars-Steenhoff like this.
  2. Torbach78

    Torbach78

    Joined:
    Aug 10, 2013
    Posts:
    296
  3. Hi_ImTemmy

    Hi_ImTemmy

    Joined:
    Jul 8, 2015
    Posts:
    174
    Could you step me through that in more detail please? With the render texture approach I'm stretching it out to cover the entire screen so I'm unsure how I'd mix in anything which wasn't affected by it. A few people I've talked to have alluded to a multi-camera approach but suggested it has drawbacks around depth of objects.

    If there are any tutorials or examples you can link me to that would be fantastic!
     
    Last edited: Feb 6, 2019
  4. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,375
    There's an approach to handle off screen particles that render at a reduced resolution that takes the depth texture from the main camera and does the depth test in shader or otherwise blits the depth to the second camera to allow for mis-matched resolutions. Usually this is going from a high resolution camera's depth to a low resolution, and then compositing the low back into the high.

    See this project (which may or may not work anymore).
    https://github.com/slipster216/OffScreenParticleRendering

    For you this would be taking a low resolution camera's depth to a higher resolution target, but the idea is the same. This would allow you to handle depth occlusions between miss-matched resolutions, at least between the main scene and one higher resolution layer. This really only works for sorting against opaque stuff though. The higher resolution stuff will render over anything that's transparent.
     
  5. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,375
    Here's an example I know does work* as I wrote it as an example for someone else recently. Kind of an ugly implementation, but functional.
     

    Attached Files:

  6. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,375
    Example of what the above project looks like (with an added line to make the offscreen texture use point sampling). It's the opposite of what you're looking to do, but again, could be made to work.
    upload_2019-2-7_0-41-47.png
     
    Hi_ImTemmy likes this.
  7. Hi_ImTemmy

    Hi_ImTemmy

    Joined:
    Jul 8, 2015
    Posts:
    174
    Hello bgolus!

    This looks really encouraging. When you say you added a line to make the offscreen texture use point sampling, where and what does that line actually look like?

    Thanks!
     
  8. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,375
    Line 68 of LowResOffscreenCamera.cs add:
    offscreenRT.filterMode = FilterMode.Point;
     
  9. Hi_ImTemmy

    Hi_ImTemmy

    Joined:
    Jul 8, 2015
    Posts:
    174
    Ah, think I figured it out!

    upload_2019-2-7_21-10-44.png
     
  10. Hi_ImTemmy

    Hi_ImTemmy

    Joined:
    Jul 8, 2015
    Posts:
    174
    You beat me by minutes :p
     
  11. Hi_ImTemmy

    Hi_ImTemmy

    Joined:
    Jul 8, 2015
    Posts:
    174
    @bgolus I'm a bit lost when you suggest I want the opposite. What's preventing me from simply putting the majority of my scene objects on the off screen layer and then having them appear pixelated?

    The problem I'm having with the overall approach though is the object I want to maintain a decent resolution is 3D text. If left on the default layer then this fails to render in front of stuff correctly.

    Any tips, or am I looking at the precise reason why I need to do things the other way round?

    upload_2019-2-7_21-47-5.png
     
  12. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,375
    The idea behind this technique is you can composite opaque or transparent things against a different resolution opaque geometry. If there is transparent geometry in the first pass this fails as the second pass has nothing in the depth buffer to test against. The depth buffer is what allows opaque objects to sort against eachother properly regardless of the order they're actually rendered in, but transparencies don't write to the depth buffer and sort between eachother based on the order they're rendered, or against what has already been written to the depth buffer.

    Here's the steps I was thinking you'd probably need to do:

    1. Render low resolution main scene with main camera
    2. Copy depth from low resolution scene to a global _MainCameraDepthTexture render texture
    3. Blit depth & low res color to higher resolution target
    4. Render high resolution stuff
    Mixing low and high transparent stuff would require rendering each low resolution thing out into its own render texture and drawing them back in individually in back to front order. Plausible, but potentially very expensive. For the style you're going for it seems like keeping everything opaque in the low res would be okay for the look, or kind of ignore the rare cases of the high res stuff overlapping.
     
  13. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,375
    Basically if your text isn't drawing using an opaque shader with a shadow caster pass (what is used to fill the depth texture) it won't be able to composite against it if it's rendered first.
     
  14. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,375
    Can you post the code you're using to do the low resolution camera right now?
     
  15. Hi_ImTemmy

    Hi_ImTemmy

    Joined:
    Jul 8, 2015
    Posts:
    174
    @bgolus Sure thing. I appreciate your help. It seems like I've bitten off a bit more than I can chew (I've never touched depth buffers or such) but, if it's possible, I think it would be a really nice asthetic if it can be done.

    Here's the original setup of the camera:
    upload_2019-2-7_22-19-56.png

    Feeds into a "low res" render texture which is then referenced in a script. The script then works as follows:

    Code (CSharp):
    1. using UnityEngine;
    2. using System.Collections;
    3.  
    4. public class Pixelation : MonoBehaviour
    5. {
    6.     public RenderTexture renderTexture;
    7.  
    8.     void Start()
    9.     {
    10.         int realRatio = Mathf.RoundToInt(Screen.width / Screen.height);
    11.         renderTexture.width = NearestSuperiorPowerOf2(Mathf.RoundToInt(renderTexture.width * realRatio));
    12.     }
    13.  
    14.     void OnGUI()
    15.     {
    16.         GUI.depth = 20;
    17.         GUI.DrawTexture(new Rect(0, 0, Screen.width, Screen.height), renderTexture);
    18.     }
    19.  
    20.     int NearestSuperiorPowerOf2(int n)
    21.     {
    22.         return (int)Mathf.Pow(2, Mathf.Ceil(Mathf.Log(n) / Mathf.Log(2)));
    23.     }
    24. }
    I then have the render texture asset itself set to a filter mode of 'point' to give everything a hard pixel look. The end result is the original image in the thread.
     
  16. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,375
    Try this script. Also remove the render texture from your main camera. You control the resolution with the first setting on this script. Currently has a bug if you disable / enable it at runtime because Screen.width & height returns the wrong values and I don't know why.
    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4. using UnityEngine.Rendering;
    5.  
    6. [RequireComponent(typeof(Camera))]
    7. public class LowResOffscreenCamera : MonoBehaviour {
    8.  
    9.     public int lowResHeight = 160;
    10.     public int highResMult = 2;
    11.  
    12.     public LayerMask offscreenLayers;
    13.     public Shader depthCopyShader;
    14.     public Shader depthFillShader;
    15.     public Shader compositeShader;
    16.  
    17.     private Material depthCopyMat;
    18.     private Material depthFillMat;
    19.     private Material compositeMat;
    20.  
    21.     private new Camera camera;
    22.     private Camera lowCamera;
    23.     private Camera highCamera;
    24.  
    25.     private CommandBuffer mainCameraDepthCopy;
    26.     private CommandBuffer highFill;
    27.  
    28.     private RenderTexture lowRT;
    29.     private RenderTexture highRT;
    30.  
    31.     void Awake () {
    32.         camera = GetComponent<Camera>();
    33.  
    34.         lowCamera = new GameObject("Offscreen Camera", typeof(Camera)).GetComponent<Camera>();
    35.         lowCamera.enabled = false;
    36.         lowCamera.transform.SetParent(camera.transform, false);
    37.         lowCamera.CopyFrom(camera);
    38.         lowCamera.renderingPath = RenderingPath.Forward;
    39.         lowCamera.cullingMask ^= offscreenLayers;
    40.         lowCamera.depthTextureMode |= DepthTextureMode.Depth;
    41.  
    42.         depthCopyMat = new Material(depthCopyShader);
    43.         depthFillMat = new Material(depthFillShader);
    44.         compositeMat = new Material(compositeShader);
    45.  
    46.         mainCameraDepthCopy = new CommandBuffer();
    47.         mainCameraDepthCopy.name = "Copy Depth Texture from Main Camera";
    48.         mainCameraDepthCopy.SetGlobalTexture("_MainCameraDepthTexture", BuiltinRenderTextureType.Depth);
    49.  
    50.         lowCamera.AddCommandBuffer(CameraEvent.BeforeForwardOpaque, mainCameraDepthCopy);
    51.  
    52.         highCamera = new GameObject("Offscreen Camera", typeof(Camera)).GetComponent<Camera>();
    53.         highCamera.enabled = false;
    54.         highCamera.transform.SetParent(camera.transform, false);
    55.         highCamera.CopyFrom(camera);
    56.         highCamera.renderingPath = RenderingPath.Forward;
    57.         highCamera.cullingMask = offscreenLayers;
    58.         highCamera.depthTextureMode = DepthTextureMode.None;
    59.         highCamera.useOcclusionCulling = false;
    60.         highCamera.backgroundColor = Color.clear;
    61.         highCamera.clearFlags = CameraClearFlags.Nothing;
    62.  
    63.         highFill = new CommandBuffer();
    64.         highFill.name = "Fill Color & Depth";
    65.         highCamera.AddCommandBuffer(CameraEvent.BeforeForwardOpaque, highFill);
    66.  
    67.         camera.cullingMask = 0;
    68.     }
    69.  
    70.     void OnEnable()
    71.     {
    72.         float ratio = (float)Screen.width / (float)Screen.height;
    73.  
    74.         int lowResWidth = Mathf.RoundToInt((float)lowResHeight * ratio);
    75.         int highResWidth = Mathf.Min(Screen.width, lowResWidth * highResMult);
    76.         int highResHeight = Mathf.Min(Screen.height, lowResHeight * highResMult);
    77.  
    78.         lowRT = new RenderTexture(lowResWidth, lowResHeight, 24);
    79.         lowRT.filterMode = FilterMode.Point;
    80.         lowRT.Create();
    81.  
    82.         highRT = new RenderTexture(highResWidth, highResHeight, 24);
    83.         highRT.filterMode = FilterMode.Point;
    84.         highRT.Create();
    85.  
    86.         highFill.Clear();
    87.         highFill.Blit(lowRT, BuiltinRenderTextureType.CurrentActive);
    88.         highFill.Blit(BuiltinRenderTextureType.Depth, BuiltinRenderTextureType.CurrentActive, depthFillMat);
    89.     }
    90.  
    91.     void OnDisable()
    92.     {
    93.         Destroy(lowRT);
    94.         Destroy(highRT);
    95.     }
    96.  
    97.     void OnPreRender()
    98.     {
    99.         lowCamera.projectionMatrix = camera.projectionMatrix;
    100.         highCamera.projectionMatrix = camera.projectionMatrix;
    101.  
    102.         lowCamera.targetTexture = lowRT;
    103.         highCamera.targetTexture = highRT;
    104.     }
    105.  
    106.     void OnRenderImage(RenderTexture src, RenderTexture dst)
    107.     {
    108.         lowCamera.Render();
    109.         highCamera.Render();
    110.         Graphics.Blit(highRT, dst);
    111.     }
    112. }
     
  17. Hi_ImTemmy

    Hi_ImTemmy

    Joined:
    Jul 8, 2015
    Posts:
    174
    @bgolus Hello. Thank you so much for this. I've tried to integrate it into your test scene for now. I put the script you outlined onto the main camera. How did you envision the layers working? As in, should high res objects be on the 'offscreen layer' or low res objects?

    At the moment I'm getting a result where it isn't clear what is being affected:

    https://i.imgur.com/7QZOg7l.gifv


    Here's how the main camera looks for the above:

    upload_2019-2-10_21-59-50.png
     
  18. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,375
    In this case the higher res stuff uses the layer. Technically both the low and high res are rendered "off screen" with this script, so it's poorly named.

    Note, like with my previous version, objects in each layer won't be affected by shadows from the other. That's a lot more work.