- Home /
Render entire sprite in 3D but with depth according to position.
Hello there.
This question is hard to describe in a title, so I will show it with pictures. I am making a game that looks like a classic rpg, with sprites for characters but rendered in 3D. I am using an orthographic camera at a 45-degree angle & am making the sprites point to the camera as to not distort them, but as you can see, they clip into the 3D geometry.
I now this is just how a sprite renderer works in 3D, but I know shaders can do some pretty cool things. Is there some technique, shader or even research I can look into to fix this? I am wondering if it is possible to render the entire sprite with the 3D depth from it's position to the camera so that it will appear in front of whatever it clips into, but still behind objects when they are behind?
Thanks in advance. (Pokemon sprites just for prototyping)
Answer by Namey5 · Mar 20, 2020 at 10:25 AM
Set your sprite upright and try this shader;
// Unity built-in shader source. Copyright (c) 2016 Unity Technologies. MIT license (see license.txt)
Shader "Sprites/Isometric"
{
Properties
{
[PerRendererData] _MainTex ("Sprite Texture", 2D) = "white" {}
_Color ("Tint", Color) = (1,1,1,1)
_ViewAngle ("Camera View Angle", Float) = 45.0
[MaterialToggle] PixelSnap ("Pixel snap", Float) = 0
[HideInInspector] _RendererColor ("RendererColor", Color) = (1,1,1,1)
[HideInInspector] _Flip ("Flip", Vector) = (1,1,1,1)
[PerRendererData] _AlphaTex ("External Alpha", 2D) = "white" {}
[PerRendererData] _EnableExternalAlpha ("Enable External Alpha", Float) = 0
}
SubShader
{
Tags
{
"Queue"="Transparent"
"IgnoreProjector"="True"
"RenderType"="Transparent"
"PreviewType"="Plane"
"CanUseSpriteAtlas"="True"
}
Cull Off
Lighting Off
ZWrite Off
//ZTest Always
Blend One OneMinusSrcAlpha
Pass
{
CGPROGRAM
#pragma vertex SpriteVert
#pragma fragment SpriteFrag
#pragma target 2.0
#pragma multi_compile_instancing
#pragma multi_compile_local _ PIXELSNAP_ON
#pragma multi_compile _ ETC1_EXTERNAL_ALPHA
#include "UnityCG.cginc"
#ifdef UNITY_INSTANCING_ENABLED
UNITY_INSTANCING_BUFFER_START(PerDrawSprite)
// SpriteRenderer.Color while Non-Batched/Instanced.
UNITY_DEFINE_INSTANCED_PROP(fixed4, unity_SpriteRendererColorArray)
// this could be smaller but that's how bit each entry is regardless of type
UNITY_DEFINE_INSTANCED_PROP(fixed2, unity_SpriteFlipArray)
UNITY_INSTANCING_BUFFER_END(PerDrawSprite)
#define _RendererColor UNITY_ACCESS_INSTANCED_PROP(PerDrawSprite, unity_SpriteRendererColorArray)
#define _Flip UNITY_ACCESS_INSTANCED_PROP(PerDrawSprite, unity_SpriteFlipArray)
#endif // instancing
CBUFFER_START(UnityPerDrawSprite)
#ifndef UNITY_INSTANCING_ENABLED
fixed4 _RendererColor;
fixed2 _Flip;
#endif
float _EnableExternalAlpha;
CBUFFER_END
// Material Color.
fixed4 _Color;
struct appdata_t
{
float4 vertex : POSITION;
float4 color : COLOR;
float2 texcoord : TEXCOORD0;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
struct v2f
{
float4 vertex : SV_POSITION;
fixed4 color : COLOR;
float2 texcoord : TEXCOORD0;
UNITY_VERTEX_OUTPUT_STEREO
};
inline float4 UnityFlipSprite(in float3 pos, in fixed2 flip)
{
return float4(pos.xy * flip, pos.z, 1.0);
}
half _ViewAngle;
v2f SpriteVert(appdata_t IN)
{
v2f OUT;
UNITY_SETUP_INSTANCE_ID (IN);
UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(OUT);
float3 worldPos = mul (unity_ObjectToWorld, float4 (IN.vertex.xyz, 1)).xyz;
float3 origin = mul (unity_ObjectToWorld, float4 (0,0,0,1)).xyz;
worldPos.y -= origin.y;
const float scale = 1.0 / cos (radians (_ViewAngle));
worldPos.y = (worldPos.y * scale) + origin.y;
IN.vertex.xyz = mul (unity_WorldToObject, float4 (worldPos, 1)).xyz;
OUT.vertex = UnityFlipSprite(IN.vertex, _Flip);
OUT.vertex = UnityObjectToClipPos(OUT.vertex);
OUT.texcoord = IN.texcoord;
OUT.color = IN.color * _Color * _RendererColor;
#ifdef PIXELSNAP_ON
OUT.vertex = UnityPixelSnap (OUT.vertex);
#endif
return OUT;
}
sampler2D _MainTex;
sampler2D _AlphaTex;
fixed4 SampleSpriteTexture (float2 uv)
{
fixed4 color = tex2D (_MainTex, uv);
#if ETC1_EXTERNAL_ALPHA
fixed4 alpha = tex2D (_AlphaTex, uv);
color.a = lerp (color.a, alpha.r, _EnableExternalAlpha);
#endif
return color;
}
fixed4 SpriteFrag(v2f IN) : SV_Target
{
fixed4 c = SampleSpriteTexture (IN.texcoord) * IN.color;
c.rgb *= c.a;
return c;
}
ENDCG
}
}
}
Initially, my idea was to warp the mesh straight in the shader and handle occlusion manually with the depth texture, but I soon realised that all that is unnecessary. Instead, you can keep your sprites standing up straight (meaning occlusion works fine), but because this is an orthographic camera with a fixed angle of 45 degrees we can just scale the sprite directly in the shader using some trigonometry. The great thing about this technique is that everything just works, and the camera doesn't have to be at an angle of 45 degrees; so long as you pass the camera's view angle (which I've exposed as a material parameter) into the shader it will work for anything.
Thank you so much for replying! Your solution mirrors that of solutions I have later found by changing my searching, but in a much cleaner way. This technique of scaling an upright sprite works well enough for most foreseeable use cases, however, there is an issue of wanting to take advantage of a 3D environment that the taller than normal sprite may interfere with what would otherwise be perfectly fine geometry. For example, going under what would normally be a high enough bridge now clips the character through it, even though he isn't that tall (seen in the first 2 pictures).
I sincerely appreciate your help. Right now I am just prototyping to feel out my limitations, so this may very well what I work with, but an ability to properly display a sprite in 3D without clipping would be ideal. An example of what I'd like to achieve would be like what is found at the bottom of this post link text) :
Ah, that's an edge case I didn't think about. In that case, my original plan should solve that issue, although to make things easier I'll combine the two approaches so you don't have to do much;
struct v2f
{
float4 vertex : SV_POSITION;
fixed4 color : COLOR;
float2 texcoord : TEXCOORD0;
float4 screenPos : TEXCOORD1;
UNITY_VERTEX_OUTPUT_STEREO
};
...
v2f SpriteVert(appdata_t IN)
{
v2f OUT;
UNITY_SETUP_INSTANCE_ID (IN);
UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(OUT);
OUT.screenPos.z = -mul (UNITY_$$anonymous$$ATRIX_$$anonymous$$V, float4 (IN.vertex.xyz, 1)).z;
float3 worldPos = mul (unity_ObjectToWorld, float4 (IN.vertex.xyz, 1)).xyz;
float3 origin = mul (unity_ObjectToWorld, float4 (0,0,0,1)).xyz;
worldPos.y -= origin.y;
const float scale = 1.0 / cos (radians (_ViewAngle));
worldPos.y = (worldPos.y * scale) + origin.y;
IN.vertex.xyz = mul (unity_WorldToObject, float4 (worldPos, 1)).xyz;
OUT.screenPos.xyw = UnityObjectToClipPos (IN.vertex).xyw;
OUT.screenPos.xy = OUT.screenPos.xy * 0.5 + OUT.screenPos.w * 0.5;
#if UNITY_UV_STARTS_AT_TOP
OUT.screenPos.y = OUT.screenPos.w - OUT.screenPos.y;
#endif
OUT.vertex = UnityFlipSprite(IN.vertex, _Flip);
OUT.vertex = UnityObjectToClipPos(OUT.vertex);
OUT.texcoord = IN.texcoord;
OUT.color = IN.color * _Color * _RendererColor;
#ifdef PIXELSNAP_ON
OUT.vertex = UnityPixelSnap (OUT.vertex);
#endif
return OUT;
}
sampler2D _$$anonymous$$ainTex;
sampler2D _AlphaTex;
sampler2D_float _CameraDepthTexture;
...
float LinearDepth (float rawDepth)
{
if (unity_CameraProjection[3][3] == 0.0)
return LinearEyeDepth (rawDepth);
#ifdef UNITY_REVERSED_Z
rawDepth = 1.0 - rawDepth;
#endif
return rawDepth * _ProjectionParams.z;
}
fixed4 SpriteFrag(v2f IN) : SV_Target
{
float2 uv = IN.screenPos.xy / IN.screenPos.w;
float depth = LinearDepth (tex2D (_CameraDepthTexture, uv).r);
clip (depth - IN.screenPos.z);
fixed4 c = SampleSpriteTexture (IN.texcoord) * IN.color;
c.rgb *= c.a;
return c;
}
Can't post the whole shader due to character limits, but here are the sections that have changed. It is 100% a hack, and you will probably have to move your sprite slightly higher than the ground on ortho cameras so it doesn't clip through the floor, but it should do the job. The problem is that if you scale the mesh, but clip against the original position you end up with false occlusion around contact edges, so you just get weird perspective holes. This is about as close as I could get, and it does work but requires a little bit of fiddling.
Also note that you will probably need to enable the depth texture on your camera (as it most likely isn't in use by default). You can use this little script;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[ExecuteInEdit$$anonymous$$ode]
public class ForceDepthTexture : $$anonymous$$onoBehaviour
{
private void OnEnable ()
{
Camera cam = GetComponent<Camera>();
cam.depthTexture$$anonymous$$ode |= DepthTexture$$anonymous$$ode.Depth;
}
}
Answer by KitoCode · Mar 23, 2020 at 03:22 AM
No joke I am working on something extremely similar. Hit me up on discord id love to talk. I believe i have the solution down to pixel perfect with the exception of a small bug.
Pyroh#9393
Did you guys ever solve this? I am getting involved in a project where I would like to do this exact projection method / sprite sorting thing.