Skip to content

Instantly share code, notes, and snippets.

@lyuma
Last active June 21, 2023 16:20
Show Gist options
  • Star 11 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save lyuma/deb04425a7cbf290c77937344b9bb70e to your computer and use it in GitHub Desktop.
Save lyuma/deb04425a7cbf290c77937344b9bb70e to your computer and use it in GitHub Desktop.
Shows a camera of your own avatar, with support for stereo and separate desktop view.
/*
AvatarCam.shader, version 8.5
Shows a camera of your own avatar, with support for stereo and separate desktop view.
Copyright (c) 2019-2022 Lyuma <xn.lyuma@gmail.com>, Smash-ter and others
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
Shader "LyumaShader/AvatarCam"
{
Properties
{
[Header(Overlay color and blending)] [Space(5)]
_Color ("Multiply color/alpha", Color) = (1,1,1,1)
_VRColor ("VR Multiply color/alpha", Color) = (1,1,1,0.1)
_Cutoff ("Cutoff", Float) = 0.02
[ToggleUI]_AlphaToMask("Use Alpha to Coverage (A2C)", Int) = 0
//UnityEngine.Rendering.BlendMode
[Enum(Zero,0,OneMinusSrcAlpha,10)] _DstAlpha("Use Zero if A2C", Int) = 0
[Header(Render Textures)] [Space(5)]
_MainTex ("Texture (to flip: tilingX=-1 offsetX=1)", 2D) = "black" {}
[NoScaleOffset] _RightEyeMainTex ("Right Eye Texture (Optional)", 2D) = "black" {}
[Header(Desktop Mode Position)] [Space(5)]
_CamOffsetX ("Camera Offset X", Range (-2, 2)) = 1
_CamOffsetY ("Camera Offset Y", Range (-2, 2)) = -1
_CamScale ("Camera Size", Range (0.01, 1)) = .25
[Header(VR Mode Position and Scale and IPD)] [Space(5)]
_VRCenterFactor ("VR View Centering", Range(1.0, 5.0)) = 2.0
_VRAspect ("VR OBS Aspect (16:9 -> 1.77)", Float) = 1.77
[PowerSlider(2.0)] _VRScale ("VR Scale", Range(0.1, 5.0)) = 2.0
[PowerSlider(10.0)] _VRDistCM ("VR Converge (cm)", Range(37, 10000)) = 1.0
[Header(Conditionally Disable)] [Space(5)]
[ToggleUI]_ShowOnlyInHead("Show only inside head (head scale < 0.01)", Float) = 0
[Enum(Always,0,VROnly,1,VROnlyLeftEye,2,VROnlyRightEye,3,DesktopOrLeftEyeVR,4,DesktopOrRightEyeVR,5,DesktopOnly,6)]
_ShowOnlyVRType("Additional show condition", Float) = 6
}
SubShader
{
Tags { "RenderType"="Transparent" "Queue"="Overlay" "PreviewType"="Plane" }
LOD 100
Cull Off
ZTest Always
ZWrite On
Blend One [_DstAlpha]
AlphaToMask [_AlphaToMask]
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
UNITY_VERTEX_INPUT_INSTANCE_ID //Insert
};
struct v2f
{
float3 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
UNITY_VERTEX_OUTPUT_STEREO //Insert
};
UNITY_DECLARE_DEPTH_TEXTURE(_MainTex);
float4 _MainTex_ST;
float4 _MainTex_TexelSize;
UNITY_DECLARE_DEPTH_TEXTURE(_RightEyeMainTex);
float4 _RightEyeMainTex_TexelSize;
float4 _Color;
float4 _VRColor;
float _CamOffsetX;
float _CamOffsetY;
float _CamScale;
float _VRCenterFactor;
float _VRAspect;
float _VRDistCM;
float _VRScale;
float _DstAlpha;
float _AlphaToMask;
static float2 camOffsetDesk = .5 + .5 * float2(_CamOffsetX, _CamOffsetY);
#ifdef UNITY_SINGLE_PASS_STEREO
static bool isInMirror = 0;
static bool isDesktop = (distance(unity_StereoWorldSpaceCameraPos[0], unity_StereoWorldSpaceCameraPos[1]) == 0.0);
static float2 camOffset = isDesktop ? camOffsetDesk : .5 + .5 * float2(_CamOffsetX, -_CamOffsetY) / _VRCenterFactor;
//static float3 centerCameraPos = lerp(unity_StereoWorldSpaceCameraPos[0], unity_StereoWorldSpaceCameraPos[1], 0.5)
//static float4x4 centerMatrix = lerp(unity_StereoCameraToWorld[0], unity_StereoCameraToWorld[1], 0.5);
#else
static bool isInMirror = (unity_CameraProjection[2][0] != 0.f || unity_CameraProjection[2][1] != 0.f);
static bool isDesktop = true;
static float2 camOffset = camOffsetDesk;
//static float4x4 mixedMatrix = (float4x4)0.0;
#endif
static float2 camScale = float2(1,1) * _CamScale * (isDesktop ? 2.0 : _VRScale / sqrt(_VRCenterFactor));
static float aspectAdjustVR = (isDesktop ? 1.0 : _VRAspect);
static float2 relativeAspect = abs(_MainTex_ST.xy) * _MainTex_TexelSize.zw / _ScreenParams.xy;
static float2 relativeAspectRatio = min(1.0, relativeAspect.xy / relativeAspect.yx);
static float2 correctedScale = camScale * min(1.0, relativeAspectRatio);
float _ShowOnlyInHead;
float _ShowOnlyVRType;
float _Cutoff;
v2f vert(appdata v)
{
float2 screenEdge = float2(min(1,aspectAdjustVR),min(1,1/aspectAdjustVR)) - .5 * correctedScale;
screenEdge = float2(
lerp(-screenEdge.x, screenEdge.x, camOffset.x),
lerp(-screenEdge.y, screenEdge.y, camOffset.y));
v2f o;
UNITY_SETUP_INSTANCE_ID(v); //Insert
UNITY_INITIALIZE_OUTPUT(v2f, o); //Insert
UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o); //Insert
//float2 thisvertex = float2(v.vertex.x, v.vertex.y);
float2 thisvertex = float2(v.uv.x, v.uv.y) - 0.5;
if (isDesktop) {
if (_ProjectionParams.x < 0.0) {
thisvertex.y = -thisvertex.y;
screenEdge.y = -screenEdge.y;
}
} else {
thisvertex.y = -thisvertex.y;
}
float3x3 camera_offset = float3x3(correctedScale.x,0,screenEdge.x,0,correctedScale.y,screenEdge.y,0,0,1);
o.vertex = float4(mul(camera_offset, float3(thisvertex.xy, 1)), 0).xyzz;
if ((_ShowOnlyVRType == 0 || _ShowOnlyVRType == 1) && !isDesktop) {
#if defined(UNITY_SINGLE_PASS_STEREO)
float distanceMeters = _VRDistCM / 100.0;
// Bug: Vertices jump from the center of one eye to the center of the other eye. Not sure what causes that.
float4 projSpaceVert0 = mul(unity_StereoCameraProjection[0], float4(o.vertex.xyw, distanceMeters).xywz);
projSpaceVert0 = float4(o.vertex.xyw, projSpaceVert0.z * o.vertex.w / projSpaceVert0.w).xywz;
float4 viewSpaceVert0 = mul(unity_StereoCameraInvProjection[0], projSpaceVert0);
viewSpaceVert0 = float4(sign(o.vertex.xy) * float2(1,-1) * abs(viewSpaceVert0.xy/viewSpaceVert0.w), distanceMeters, 1.0);
float3 worldSpaceInVertex0 = mul(unity_StereoCameraToWorld[0], viewSpaceVert0).xyz;
float4 projSpaceVert1 = mul(unity_StereoCameraProjection[1], float4(o.vertex.xyw, distanceMeters).xywz);
projSpaceVert1 = float4(o.vertex.xyw, projSpaceVert1.z * o.vertex.w / projSpaceVert1.w).xywz;
float4 viewSpaceVert1 = mul(unity_StereoCameraInvProjection[1], projSpaceVert1);
viewSpaceVert1 = float4(sign(o.vertex.xy) * float2(1,-1) * abs(viewSpaceVert1.xy/viewSpaceVert1.w), distanceMeters, 1.0);
float3 worldSpaceInVertex1 = mul(unity_StereoCameraToWorld[1], viewSpaceVert1).xyz;
float3 worldSpaceVert = lerp(worldSpaceInVertex0, worldSpaceInVertex1, 0.5);
o.vertex = UnityWorldToClipPos(float4(worldSpaceVert, 1.0));
o.vertex.z = o.vertex.w;
#endif
}
float4 hasData = true;
if (_ShowOnlyInHead > 0 && length(mul((float3x3)unity_ObjectToWorld, float3(0,0,1)).xyz) > 0.01) {
o.vertex = float4(1,1,1,1);
}
if (any(_MainTex_TexelSize.zw == _ScreenParams.xy)) {
o.vertex = float4(1,1,1,1);
}
if (all(hasData == 0) || any(_MainTex_TexelSize.zw < float2(64,64))) {
o.vertex = float4(1,1,1,1);
}
if (isInMirror) {
o.vertex = float4(1,1,1,1);
}
#if defined(UNITY_SINGLE_PASS_STEREO)
if ((_ShowOnlyVRType == 6 && !isDesktop) ||
((_ShowOnlyVRType == 1|| _ShowOnlyVRType == 2 || _ShowOnlyVRType == 3) && isDesktop) ||
(_CamScale * _VRScale) > 1.5 ||
((_ShowOnlyVRType == 2 || _ShowOnlyVRType == 4) && !isDesktop && unity_StereoEyeIndex != 0) ||
((_ShowOnlyVRType == 3 || _ShowOnlyVRType == 5) && !isDesktop && unity_StereoEyeIndex != 1))
#else
if (_ShowOnlyVRType == 1 || _ShowOnlyVRType == 2 || _ShowOnlyVRType == 3)
#endif
{
o.vertex = float4(1,1,1,1);
}
o.uv.xy = TRANSFORM_TEX(v.uv, _MainTex);
o.uv.z = 0.0;
if (!isDesktop && !any(_RightEyeMainTex_TexelSize.zw < float2(64,64))) {
#if defined(UNITY_SINGLE_PASS_STEREO)
float4 hasRightEyeData = tex2Dlod(_RightEyeMainTex, float4(.5,.5,0,0)) +
tex2Dlod(_RightEyeMainTex, float4(.51,.4,0,0)) + tex2Dlod(_RightEyeMainTex, float4(.4,.51,0,0)) +
tex2Dlod(_RightEyeMainTex, float4(.61,.61,0,0)) + tex2Dlod(_RightEyeMainTex, float4(.3,.71,0,0));
if (!all(hasRightEyeData == 0) && unity_StereoEyeIndex == 1) {
o.uv.z = 1.0;
}
#endif
}
return o;
}
fixed4 frag (v2f i) : SV_Target
{
UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(i); //Insert
fixed4 col = UNITY_SAMPLE_SCREENSPACE_TEXTURE(_MainTex, i.uv); //Insert
if (i.uv.z > 0.5) {
col = UNITY_SAMPLE_SCREENSPACE_TEXTURE(_RightEyeMainTex, i.uv.xy);
}
col.a = saturate(col.a);
clip(col.a - _Cutoff);
col *= (isDesktop ? _Color : _VRColor);
col.rgb *= (_DstAlpha > 2 ? col.a : 1.0);
col.a = (_AlphaToMask > 0.5 ? sqrt(col.a) : col.a);
return col;
}
ENDCG
}
}
}
@BMoankee
Copy link

BMoankee commented Sep 9, 2021

Please can you make a simple tutorial on how to set this up? I've played with it a bit but can't seem to figure it out. Do you need a duplicate of your avatar in unity?

@lyuma
Copy link
Author

lyuma commented Sep 9, 2021

I am releasing a prefab. Thanks to Smash-ter and BMoankee and others for testing it. <3
But first of all, make sure you have layers set up,

To do this, go to Edit -> Project Settings... then click Tags and Layers on the left pane.
The only important ones for avatars are:
9 -> Player
10 -> PlayerLocal
12 -> UIMenu
18 -> MirrorReflection

  • PlayerLocal = your local avatar with head chopped off
  • MirrorReflection = your local avatar as it appears in mirrors and cameras
  • Player = remote players
  • UIMenu = auxiliary layer that can be used for avatar UI (for example, a camera preview)
    After doing that, you can try this prefab I made for the avatar camera hud (it shows a 3d transparent copy of your avatar in front of you)

Once your layers are installed, here is the prefab I made which you can simply place on your avatar:
LyumaAvatarCam_v0.2.unitypackage (mirror)

It supports using two cameras (so you can see yourself in stereo), but you are free to disable the right camera.
If it's just for stream, you don't need the second. Also, you can make it only for stream camera by selecting desktop only, and play vrchat in "Steadycam" mode from the Camera menu

To test in the editor, select your meshes only (body etc) and change the layer to MirrorReflection on the top right dropdown in the inspector.

A note about the cameras: The shader will break if the camera background color is exactly black! The cameras in the prefab use a background of #030303 color which looks close to black but avoids the issue.

Let me know on Discord (#0781) if you have any trouble using this shader.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment