-
-
Save lyuma/deb04425a7cbf290c77937344b9bb70e to your computer and use it in GitHub Desktop.
/* | |
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 <[email protected]>, 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 | |
} | |
} | |
} |
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.
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?