From 868e713336e6d448d91e0ad2dcff4d3864dda09d Mon Sep 17 00:00:00 2001 From: DeMuenu <96650288+DeMuenu@users.noreply.github.com> Date: Mon, 29 Sep 2025 23:14:38 +0200 Subject: [PATCH] Add shadowcaster support to lighting and shaders Introduces shadowcaster support by adding shadow map index fields to light data, updating PlayerPositionsToShader and LightdataStorage to handle shadow map indices, and extending the shader and its includes to sample shadowcaster planes. Adds ShadowcasterUpdater script and editor preview for updating world-to-local matrices, and updates relevant arrays and property handling throughout the codebase. Also adds a sample plane mesh for shadowcasting. --- .../Editor/PlayerPositionsToShaderPreview.cs | 53 ++++--- .../Editor/ShadowcasterUpdaterPreview.cs | 142 ++++++++++++++++++ .../Editor/ShadowcasterUpdaterPreview.cs.meta | 11 ++ .../PlayerPositionsToShader.Editor.cs | 60 ++++---- Mesh.meta | 8 + Mesh/Plane.fbx | Bin 0 -> 11580 bytes Mesh/Plane.fbx.meta | 109 ++++++++++++++ Scripts/LightdataStorage.cs | 4 + Scripts/PlayerPositionsToShader.cs | 62 ++++++-- Scripts/ShadowcasterUpdater.cs | 35 +++++ Scripts/ShadowcasterUpdater.cs.meta | 11 ++ Shader/BlendinShader.shader | 23 ++- Shader/Includes/Shadowcaster.cginc | 56 +++++++ Shader/Includes/Shadowcaster.cginc.meta | 7 + Shader/Includes/Variables.hlsl | 1 + 15 files changed, 521 insertions(+), 61 deletions(-) create mode 100644 EditorPreview/Editor/ShadowcasterUpdaterPreview.cs create mode 100644 EditorPreview/Editor/ShadowcasterUpdaterPreview.cs.meta create mode 100644 Mesh.meta create mode 100644 Mesh/Plane.fbx create mode 100644 Mesh/Plane.fbx.meta create mode 100644 Scripts/ShadowcasterUpdater.cs create mode 100644 Scripts/ShadowcasterUpdater.cs.meta create mode 100644 Shader/Includes/Shadowcaster.cginc create mode 100644 Shader/Includes/Shadowcaster.cginc.meta diff --git a/EditorPreview/Editor/PlayerPositionsToShaderPreview.cs b/EditorPreview/Editor/PlayerPositionsToShaderPreview.cs index 321c96e..716a6f1 100644 --- a/EditorPreview/Editor/PlayerPositionsToShaderPreview.cs +++ b/EditorPreview/Editor/PlayerPositionsToShaderPreview.cs @@ -17,7 +17,8 @@ public static class PlayerPositionsToShaderPreview public Vector4[] colors; public Vector4[] directions; public float[] types; - public int size; + public float[] shadowMapIndices; + public int size; } static PlayerPositionsToShaderPreview() @@ -66,16 +67,17 @@ public static class PlayerPositionsToShaderPreview static void EnsureArrays(PlayerPositionsToShader src, int required) { if (!_cache.TryGetValue(src, out var c) || - c.positions == null || c.colors == null || c.directions == null || c.types == null || + c.positions == null || c.colors == null || c.directions == null || c.types == null || c.shadowMapIndices == null || c.size != required) { c = new Cache { - positions = new Vector4[required], - colors = new Vector4[required], - directions = new Vector4[required], - types = new float[required], - size = required + positions = new Vector4[required], + colors = new Vector4[required], + directions = new Vector4[required], + types = new float[required], + shadowMapIndices = new float[required], + size = required }; _cache[src] = c; } @@ -87,25 +89,27 @@ public static class PlayerPositionsToShaderPreview EnsureArrays(src, max); var c = _cache[src]; - var positions = c.positions; - var colors = c.colors; - var directions = c.directions; - var types = c.types; + var positions = c.positions; + var colors = c.colors; + var directions = c.directions; + var types = c.types; + var shadowMapIndices = c.shadowMapIndices; // Clear arrays to safe defaults for (int i = 0; i < max; i++) { - positions[i] = Vector4.zero; - colors[i] = Vector4.zero; - directions[i] = Vector4.zero; - types[i] = 0f; + positions[i] = Vector4.zero; + colors[i] = Vector4.zero; + directions[i] = Vector4.zero; + types[i] = 0f; + shadowMapIndices[i] = 0f; } // Use the Editor-side function defined on the partial class int count = 0; try { - src.Editor_BuildPreview(out positions, out colors, out directions, out types, out count); + src.Editor_BuildPreview(out positions, out colors, out directions, out types, out shadowMapIndices, out count); // replace cache arrays if sizes changed if (positions.Length != c.size) @@ -113,11 +117,12 @@ public static class PlayerPositionsToShaderPreview _cache[src] = new Cache { - positions = positions, - colors = colors, - directions = directions, - types = types, - size = positions.Length + positions = positions, + colors = colors, + directions = directions, + types = types, + shadowMapIndices = shadowMapIndices, + size = positions.Length }; } catch @@ -152,6 +157,12 @@ public static class PlayerPositionsToShaderPreview Shader.SetGlobalFloatArray(id, types); } + if (!string.IsNullOrEmpty(src.shadowMapIndexProperty)) + { + int id = Shader.PropertyToID(src.shadowMapIndexProperty); + Shader.SetGlobalFloatArray(id, shadowMapIndices); + } + if (!string.IsNullOrEmpty(src.countProperty)) { int id = Shader.PropertyToID(src.countProperty); diff --git a/EditorPreview/Editor/ShadowcasterUpdaterPreview.cs b/EditorPreview/Editor/ShadowcasterUpdaterPreview.cs new file mode 100644 index 0000000..6c604c5 --- /dev/null +++ b/EditorPreview/Editor/ShadowcasterUpdaterPreview.cs @@ -0,0 +1,142 @@ +#if UNITY_EDITOR +using UnityEditor; +using UnityEngine; +using System.Collections.Generic; + +[InitializeOnLoad] +public static class ShadowcasterUpdaterPreview +{ + const double kTickInterval = 0.1; // seconds + static double _nextTick; + + // One shared MPB to avoid allocations each frame + static readonly MaterialPropertyBlock sMPB = new MaterialPropertyBlock(); + + // Cache: component -> property ID, and renderer -> last applied matrix + static readonly Dictionary _propId = new Dictionary(); + static readonly Dictionary _lastW2L = new Dictionary(); + static readonly List _toRemove = new List(32); + + static ShadowcasterUpdaterPreview() + { + EditorApplication.update += Update; + EditorApplication.hierarchyChanged += ForceTick; + Undo.undoRedoPerformed += ForceTick; + Selection.selectionChanged += ForceTick; + EditorApplication.playModeStateChanged += _ => ForceTick(); + } + + public static void ForceTick() => _nextTick = 0; + + static void Update() + { +#if UNITY_2019_1_OR_NEWER + if (EditorApplication.isPlayingOrWillChangePlaymode) return; +#else + if (EditorApplication.isPlaying) return; +#endif + double now = EditorApplication.timeSinceStartup; + if (now < _nextTick) return; + _nextTick = now + kTickInterval; + + CleanupNullRenderers(); + + var behaviours = FindAllInScene(); + foreach (var b in behaviours) + { + if (b == null || !b.isActiveAndEnabled) continue; + if (EditorUtility.IsPersistent(b)) continue; // skip assets/prefabs + ApplyToBehaviour(b); + } + + SceneView.RepaintAll(); + } + + static ShadowcasterUpdater[] FindAllInScene() + { +#if UNITY_2023_1_OR_NEWER + return Object.FindObjectsByType(FindObjectsInactive.Exclude, FindObjectsSortMode.None); +#elif UNITY_2020_1_OR_NEWER + return Object.FindObjectsOfType(true); +#else + return Resources.FindObjectsOfTypeAll(); +#endif + } + + static void CleanupNullRenderers() + { + _toRemove.Clear(); + foreach (var kv in _lastW2L) + if (kv.Key == null) _toRemove.Add(kv.Key); + for (int i = 0; i < _toRemove.Count; i++) + _lastW2L.Remove(_toRemove[i]); + } + + static int GetPropertyId(ShadowcasterUpdater b) + { + string name = string.IsNullOrEmpty(b.propertyName) ? "_Udon_WorldToLocal" : b.propertyName; + + if (!_propId.TryGetValue(b, out int id)) + { + id = Shader.PropertyToID(name); + _propId[b] = id; + return id; + } + + // If user changed the property name in inspector, refresh the ID + int newId = Shader.PropertyToID(name); + if (newId != id) + { + _propId[b] = newId; + id = newId; + } + return id; + } + + static void ApplyToBehaviour(ShadowcasterUpdater b) + { + var renderers = b.rendererTargets; + if (renderers == null || renderers.Length == 0) return; + + int id = GetPropertyId(b); + Matrix4x4 w2l = b.transform.worldToLocalMatrix; + + for (int i = 0; i < renderers.Length; i++) + { + Renderer r = renderers[i]; + if (r == null) continue; + + if (_lastW2L.TryGetValue(r, out var last) && last == w2l) + continue; // nothing changed for this renderer + + r.GetPropertyBlock(sMPB); + sMPB.SetMatrix(id, w2l); + r.SetPropertyBlock(sMPB); + + _lastW2L[r] = w2l; + } + } +} + +[CustomEditor(typeof(ShadowcasterUpdater))] +public class ShadowcasterUpdaterInspector : Editor +{ + public override void OnInspectorGUI() + { + DrawDefaultInspector(); + GUILayout.Space(6); + using (new EditorGUI.DisabledScope(true)) + { + EditorGUILayout.LabelField("Edit-Mode Preview", EditorStyles.boldLabel); + EditorGUILayout.LabelField("Keeps the matrix property updated in the Scene View."); + } + + if (GUILayout.Button("Refresh Now")) + { + ShadowcasterUpdaterPreview.ForceTick(); + EditorApplication.QueuePlayerLoopUpdate(); + SceneView.RepaintAll(); + } + } +} +#endif diff --git a/EditorPreview/Editor/ShadowcasterUpdaterPreview.cs.meta b/EditorPreview/Editor/ShadowcasterUpdaterPreview.cs.meta new file mode 100644 index 0000000..db780ad --- /dev/null +++ b/EditorPreview/Editor/ShadowcasterUpdaterPreview.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0491ab6ce27c5ba449e98288f0e3d8ed +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/EditorPreview/PlayerPositionsToShader.Editor.cs b/EditorPreview/PlayerPositionsToShader.Editor.cs index 9d5e592..bb57b34 100644 --- a/EditorPreview/PlayerPositionsToShader.Editor.cs +++ b/EditorPreview/PlayerPositionsToShader.Editor.cs @@ -9,44 +9,52 @@ public partial class PlayerPositionsToShader out Vector4[] colors, out Vector4[] directions, out float[] types, + out float[] shadowMapIndices, out int count) { int max = Mathf.Max(1, maxLights); - positions = new Vector4[max]; - colors = new Vector4[max]; - directions = new Vector4[max]; - types = new float[max]; - count = 0; + positions = new Vector4[max]; + colors = new Vector4[max]; + directions = new Vector4[max]; + types = new float[max]; + shadowMapIndices = new float[max]; + count = 0; - // ✅ Avoid Array.Empty(); just guard the loop - if (otherLightSources != null) + if (otherLightSources == null) return; + + for (int i = 0; i < otherLightSources.Length && count < max; i++) { - for (int i = 0; i < otherLightSources.Length && count < max; i++) - { - Transform t = otherLightSources[i]; - if (t == null || !t.gameObject.activeInHierarchy) continue; + Transform t = otherLightSources[i]; + if (t == null || !t.gameObject.activeInHierarchy) continue; - LightdataStorage data = t.GetComponent(); + LightdataStorage data = t.GetComponent(); - Vector3 pos = t.position; - float range = (data != null) ? data.range * t.localScale.x : t.localScale.x; - Vector4 col = (data != null) ? data.GetFinalColor() : new Vector4(1f, 1f, 1f, 1f); - float intens = (data != null) ? data.intensity * t.localScale.x : 1f; - float cosHalf = (data != null) ? data.GetCosHalfAngle() : 1f; - int typeId = (data != null) ? data.GetTypeId() : 0; + Vector3 pos = t.position; + float range = (data != null) ? data.range * t.localScale.x : t.localScale.x; + // rgb = color, a = intensity (packed to match runtime/shader) + Vector4 col = (data != null) ? data.GetFinalColor() : new Vector4(1f, 1f, 1f, 1f); + float intensity = (data != null) ? data.intensity * t.localScale.x : 1f; - Quaternion rot = t.rotation; - Vector3 fwd = rot * Vector3.down; + // w = cosHalfAngle (0 for omni) + float cosHalf = (data != null) ? data.GetCosHalfAngle() : 0f; - positions[count] = new Vector4(pos.x, pos.y, pos.z, range); - colors[count] = new Vector4(col.x, col.y, col.z, intens); - directions[count] = new Vector4(fwd.x, fwd.y, fwd.z, data.spotAngleDeg); - types[count] = (float)typeId; + // 0=Omni, 1=Spot, 2=Directional (your custom enum) + int typeId = (data != null) ? data.GetTypeId() : 0; - count++; - } + float shIndex = (data != null) ? data.shadowMapIndex : 0f; + + Quaternion rot = t.rotation; + Vector3 fwd = rot * Vector3.down; + + positions[count] = new Vector4(pos.x, pos.y, pos.z, range); + colors[count] = new Vector4(col.x, col.y, col.z, intensity); + directions[count] = new Vector4(fwd.x, fwd.y, fwd.z, cosHalf); + types[count] = (float)typeId; + shadowMapIndices[count] = shIndex; + + count++; } } } diff --git a/Mesh.meta b/Mesh.meta new file mode 100644 index 0000000..96703d0 --- /dev/null +++ b/Mesh.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 2d573c2258a40a04f9932e8910763931 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Mesh/Plane.fbx b/Mesh/Plane.fbx new file mode 100644 index 0000000000000000000000000000000000000000..4371e312c3cee84f87299a7d10b27467a7187d73 GIT binary patch literal 11580 zcmc&)eQX@X6<-HCc5F9*TPV=9Tp+;-1m^;z;iHmszKfG1=MTKLn?!D5eK)o@+1ows z?wNBz6bk&;imFuNL$v?30#!&Ulp;|iD5^?CTcx5!6)i#$Z9}3!2`yC-A8LPZW_EAR zzTG?9e{|&a?#;~m&Ag9!@6GJ)Hx>$pYa|8-4kQLl+i({W3GEW?)CNuabGsIn9<=Xc zMuE9|Dn7G4)3K+}*n+DK*Lh*{By*`Td*&QXt0+A>(IaRh@Q0PQ^?U5RyHN5$H@;9} zSu|<)5VS1kr(I_7R4>H5eo(316D79@vj_uE7*P-fs zKm;h~WQ>LE7g5=(RJQJO%5Js?wf#!%+9A^}`z*T)^bC$@OLF>Nufq3p=k#MO!G`gZnRlgN$hc;4B! zB;Ts!4NwfM4u2xcj zuD0GNTS?8Ks0;Yf32_IdV~F_%Va-ML{-)B7>pA4H{fa+)w^)ZIQcT6Ot_X#WczjBHSJlQL_&4AX}sh183*Eb`(~yqKa((D zK^tJ!Qx`YC5~PzrY6bZwfkfIGam`t*UW=;^ES8+IT@ceitF{~6iG>_4jC~1i0mYrE zQpqy&e0@#A;vi?0BlOe+4lRSgCI<+GbpkDKU^UJYP5FSs_Bfm#wYg zizF42tWv{wOXyKCn~S5MWB7|v@gF4m9)XU?5H)cgSDMlBcGwvts0|Cq3u_<)ovu*H zPfP51f&C@iLiJR_|~O!#^brf-}g7wkYrC=?W>7a|`B7RWBBJF8OnG}>1c&n5R$9;F{xM>GrFn(N&p*@WE6uM!KWFZ+gW0WmFjY#Sm5pLyk=!ev$P|hQ9 z9EEQwA)kULx){@CFw*(qlK`SIYs8X z3mf51>SkWFfTXRV$)F$(33KX)9ErIFFYkU91Vw8hi#FgHmHc zCnamrmr_c1OBwd(+H}@}oW?CV+#XxevdEbeU_UfKhO!?Q^g)Yg%<&CcnIkhZ9`jeK z=!Ycm%>tNo6#$Q#^UjKK-&=}HQ68z`>gGELe?o%Diusa8B<`xF`z^qd()$D~d4h4* zus!GmA{MT?p4pb3#AXrZaVrhOoa*T)-C9*m-V zs}bd)D9R03Eitn-Dv`6DY(yD3+wV4_jGXN;NvSx$U_p(zI1c1o)>+}**ya+KU&e(M zPP_Dg4k=rn4LWYlESmm^ZPj(W=&wykjGF`omS7dbH)m#S=6MGiCtaHM!zO?ZHUso_ z6F`TW0osf#pTv_CtkJTd?NW}}jj1>7z5~=mB6EC7k4Ie=%9pZ;6@=YaC92|!Mjg*z zEo>Lg;E2_$a4qun@s7v<;UX@BxvatqJsX3%au@Uu$>dhSB>8b=u8}KxGu8#kraoEZ za-NDMi(JbO#*oFhomXPXA}85`Q{5=uj7sDrcS*7>LXu?LS$mGTrqA>3h=jtCREnFD zaWvk>eg!(RvAtq6%A_RSDopHR9Ct>5HfLdH`!kpbit;c5P!gvim+DL-4eb6nsz}xS zB91CjbMHq}#aLG_&iNwvGAfZX{8}7UW`8OEQ%Yt1pko@Gr% z*pK5enhLJ{IK7UUM^kb2-Z~gYWqG{39FNfyFI|@|SmkGW>PEMHy*5{nt>usM}&^9Z(UoW~N=4iug7W>+Hh684NYT)Bo2Hy;nOC<-V z!UR|6Oy|j36l`*W^X!z5SLf68Jq5mYs6@YUp;riRN|ejRTeUgcPSQ$AFJJGx?UxdJANoP?l(Q0c z7^#+2phIKS3pR-v4+>Pu-v~8`QHy^s!K@ddCV)v|yGCGAmZlurE&5pp@o`DNN6=#v zr_d*4dTHS7SdVGqxb%-73ev-J%UiSslBHdoA$vVoF6W@`3S1q*(QodJ%*#8eBY&mD zU$ts2K_fR6I^ex!Gj$QA=5#KoR;myj$#!BA((09%%aq>NCD}5i_nai7EPBnvWT8Xyx?A3t-p+L5~1)>*A_`zWREqa4SpbyZ5x*@7wHz93)x_kPW=9E!Tk`N4ye5h4 za$NXW60bFKA8&Yv`zIc=qMoR5oaj3Q$RSN_~06+c^nk&=&%pha--p^ZN0tP z-IFh7K+8+`Z~=xRTbJ3&3i1rX+b>aIVKlcc+!~yTkj>N-9}s~dbq?XZy;(J~2b8vT zylvb#B0hWlk!&J+ZKwBLaBk_AW3raqT6?;TuSxN(AuVqCjCUe6gV+3+*jD}7m3C~K zJ_*1Z*^Q_tJ>#@VYf0-g;>bv->RdpyuWq*>V21sJ?)?58Wym(-&AS^lfjGc{i~Urc zPcUmhK6?Hq>wbUik261=|M=bw|GDBW(MaD8|G&zIhrWII&40gs*E^@X|5|=vYuEn( D{BuXL literal 0 HcmV?d00001 diff --git a/Mesh/Plane.fbx.meta b/Mesh/Plane.fbx.meta new file mode 100644 index 0000000..52d3b6f --- /dev/null +++ b/Mesh/Plane.fbx.meta @@ -0,0 +1,109 @@ +fileFormatVersion: 2 +guid: 26a14a3ac3d4388458abdbca51f4efc8 +ModelImporter: + serializedVersion: 22200 + internalIDToNameTable: [] + externalObjects: {} + materials: + materialImportMode: 2 + materialName: 0 + materialSearch: 1 + materialLocation: 1 + animations: + legacyGenerateAnimations: 4 + bakeSimulation: 0 + resampleCurves: 1 + optimizeGameObjects: 0 + removeConstantScaleCurves: 0 + motionNodeName: + rigImportErrors: + rigImportWarnings: + animationImportErrors: + animationImportWarnings: + animationRetargetingWarnings: + animationDoRetargetingWarnings: 0 + importAnimatedCustomProperties: 0 + importConstraints: 0 + animationCompression: 1 + animationRotationError: 0.5 + animationPositionError: 0.5 + animationScaleError: 0.5 + animationWrapMode: 0 + extraExposedTransformPaths: [] + extraUserProperties: [] + clipAnimations: [] + isReadable: 0 + meshes: + lODScreenPercentages: [] + globalScale: 1 + meshCompression: 0 + addColliders: 0 + useSRGBMaterialColor: 1 + sortHierarchyByName: 1 + importPhysicalCameras: 1 + importVisibility: 1 + importBlendShapes: 1 + importCameras: 1 + importLights: 1 + nodeNameCollisionStrategy: 1 + fileIdsGeneration: 2 + swapUVChannels: 0 + generateSecondaryUV: 0 + useFileUnits: 1 + keepQuads: 0 + weldVertices: 1 + bakeAxisConversion: 0 + preserveHierarchy: 0 + skinWeightsMode: 0 + maxBonesPerVertex: 4 + minBoneWeight: 0.001 + optimizeBones: 1 + meshOptimizationFlags: -1 + indexFormat: 0 + secondaryUVAngleDistortion: 8 + secondaryUVAreaDistortion: 15.000001 + secondaryUVHardAngle: 88 + secondaryUVMarginMethod: 1 + secondaryUVMinLightmapResolution: 40 + secondaryUVMinObjectScale: 1 + secondaryUVPackMargin: 4 + useFileScale: 1 + strictVertexDataChecks: 0 + tangentSpace: + normalSmoothAngle: 60 + normalImportMode: 0 + tangentImportMode: 3 + normalCalculationMode: 4 + legacyComputeAllNormalsFromSmoothingGroupsWhenMeshHasBlendShapes: 0 + blendShapeNormalImportMode: 1 + normalSmoothingSource: 0 + referencedClips: [] + importAnimation: 1 + humanDescription: + serializedVersion: 3 + human: [] + skeleton: [] + armTwist: 0.5 + foreArmTwist: 0.5 + upperLegTwist: 0.5 + legTwist: 0.5 + armStretch: 0.05 + legStretch: 0.05 + feetSpacing: 0 + globalScale: 1 + rootMotionBoneName: + hasTranslationDoF: 0 + hasExtraRoot: 0 + skeletonHasParents: 1 + lastHumanDescriptionAvatarSource: {instanceID: 0} + autoGenerateAvatarMappingIfUnspecified: 1 + animationType: 2 + humanoidOversampling: 1 + avatarSetup: 0 + addHumanoidExtraRootOnlyWhenUsingAvatar: 1 + importBlendShapeDeformPercent: 1 + remapMaterialsIfMaterialImportModeIsNone: 0 + additionalBone: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Scripts/LightdataStorage.cs b/Scripts/LightdataStorage.cs index 503f501..3c547d5 100644 --- a/Scripts/LightdataStorage.cs +++ b/Scripts/LightdataStorage.cs @@ -32,6 +32,10 @@ public class LightdataStorage : UdonSharpBehaviour [Tooltip("0 = omni (no cone)")] public float spotAngleDeg = 0f; + [Header("Shadow Settings")] + [Tooltip("0 = no shadows, 1-4 = shadow map index")] + public float shadowMapIndex = 0f; // 0 = no shadows, 1-4 = shadow map index + // Convert to a Vector4 for your shader upload public Vector4 GetFinalColor() { diff --git a/Scripts/PlayerPositionsToShader.cs b/Scripts/PlayerPositionsToShader.cs index 8e398c0..5b8b2e5 100644 --- a/Scripts/PlayerPositionsToShader.cs +++ b/Scripts/PlayerPositionsToShader.cs @@ -24,6 +24,9 @@ public partial class PlayerPositionsToShader : UdonSharpBehaviour public float playerLightIntensity = 5f; public float remoteLightIntensity = 2f; + [Tooltip("0 = no shadows, 1-4 = shadow map index")] + public float PlayerShadowMapIndex = 0f; // 0 = no shadows, 1-4 = shadow map index + [Header("Shader property names (advanced users)")] [Tooltip("Vector4 array: xyz = position, w = range")] @@ -41,6 +44,9 @@ public partial class PlayerPositionsToShader : UdonSharpBehaviour [Tooltip("float array: light type (1=area, 2=cone, etc)")] public string typeProperty = "_Udon_LightType"; + [Tooltip("float array: shadow map index (0=none, 1-4=shadow map index)")] + public string shadowMapIndexProperty = "_Udon_ShadowMapIndex"; + [Header("Max Lights (advanced users)")] [Tooltip("Hard cap / array size. 80 = default cap")] public int maxLights = 80; @@ -57,9 +63,11 @@ public partial class PlayerPositionsToShader : UdonSharpBehaviour private float[] _TypeArray; private bool _TypeArray_isDirty = false; + private float[] _ShadowMapArray; + private bool _ShadowMap_isDirty = false; private VRCPlayerApi[] _players; - private MaterialPropertyBlock _mpb; + public int currentCount { get; private set; } @@ -70,6 +78,7 @@ public partial class PlayerPositionsToShader : UdonSharpBehaviour private int UdonID_LightColors; private int UdonID_LightDirections; private int UdonID_LightType; + private int UdonID_ShadowMapIndex; void Start() { @@ -79,15 +88,16 @@ public partial class PlayerPositionsToShader : UdonSharpBehaviour _lightColors = new Vector4[maxLights]; _directions = new Vector4[maxLights]; _TypeArray = new float[maxLights]; + _ShadowMapArray = new float[maxLights]; _players = new VRCPlayerApi[maxLights]; - _mpb = new MaterialPropertyBlock(); UdonID_PlayerPositions = VRCShader.PropertyToID(positionsProperty); UdonID_LightCount = VRCShader.PropertyToID(countProperty); UdonID_LightColors = VRCShader.PropertyToID(colorProperty); UdonID_LightDirections = VRCShader.PropertyToID(directionsProperty); UdonID_LightType = VRCShader.PropertyToID(typeProperty); + UdonID_ShadowMapIndex = VRCShader.PropertyToID(shadowMapIndexProperty); UpdateData(); @@ -150,6 +160,11 @@ public partial class PlayerPositionsToShader : UdonSharpBehaviour _TypeArray[i] = 0f; _TypeArray_isDirty = true; } + if (_ShadowMapArray[i] != PlayerShadowMapIndex) + { + _ShadowMapArray[i] = PlayerShadowMapIndex; + _ShadowMap_isDirty = true; + } } @@ -175,6 +190,11 @@ public partial class PlayerPositionsToShader : UdonSharpBehaviour _TypeArray[i] = 0f; _TypeArray_isDirty = true; } + if (_ShadowMapArray[i] != 0f) + { + _ShadowMapArray[i] = 0f; + _ShadowMap_isDirty = true; + } } } @@ -230,6 +250,13 @@ public partial class PlayerPositionsToShader : UdonSharpBehaviour _TypeArray_isDirty = true; } + float shadowMapIndex = (data != null) ? data.shadowMapIndex : 0f; + if (_ShadowMapArray[currentCount] != shadowMapIndex) + { + _ShadowMapArray[currentCount] = shadowMapIndex; + _ShadowMap_isDirty = true; + } + currentCount++; } } @@ -259,6 +286,12 @@ public partial class PlayerPositionsToShader : UdonSharpBehaviour _TypeArray[i] = 0f; _TypeArray_isDirty = true; } + + if (_ShadowMapArray[i] != 0f) + { + _ShadowMapArray[i] = 0f; + _ShadowMap_isDirty = true; + } } } @@ -267,23 +300,26 @@ public partial class PlayerPositionsToShader : UdonSharpBehaviour // Snapshot which things are dirty this frame bool pushPositions = _positons_isDirty; - bool pushColors = _lightColors_isDirty; - bool pushDirs = _directions_isDirty; - bool pushTypes = _TypeArray_isDirty && !string.IsNullOrEmpty(typeProperty); + bool pushColors = _lightColors_isDirty; + bool pushDirs = _directions_isDirty; + bool pushTypes = _TypeArray_isDirty && !string.IsNullOrEmpty(typeProperty); + bool pushShadowMap = _ShadowMap_isDirty; if (pushPositions) VRCShader.SetGlobalVectorArray(UdonID_PlayerPositions, _positions); - if (pushColors) VRCShader.SetGlobalVectorArray(UdonID_LightColors, _lightColors); - if (pushDirs) VRCShader.SetGlobalVectorArray(UdonID_LightDirections, _directions); - if (pushTypes) _mpb.SetFloatArray(UdonID_LightType, _TypeArray); + if (pushColors) VRCShader.SetGlobalVectorArray(UdonID_LightColors, _lightColors); + if (pushDirs) VRCShader.SetGlobalVectorArray(UdonID_LightDirections, _directions); + if (pushTypes) VRCShader.SetGlobalFloatArray(UdonID_LightType, _TypeArray); + if (pushShadowMap) VRCShader.SetGlobalFloatArray(UdonID_ShadowMapIndex, _ShadowMapArray); VRCShader.SetGlobalFloat(UdonID_LightCount, currentCount); - + Debug.Log($"[MoonlightVRC] Pushed {currentCount} lights to shader."); // Only now mark them clean - if (pushPositions) { _positons_isDirty = false;} - if (pushColors) { _lightColors_isDirty = false;} - if (pushDirs) { _directions_isDirty = false;} - if (pushTypes) { _TypeArray_isDirty = false;} + if (pushPositions) { _positons_isDirty = false; } + if (pushColors) { _lightColors_isDirty = false; } + if (pushDirs) { _directions_isDirty = false; } + if (pushTypes) { _TypeArray_isDirty = false; } + if (pushShadowMap) { _ShadowMap_isDirty = false; } } } diff --git a/Scripts/ShadowcasterUpdater.cs b/Scripts/ShadowcasterUpdater.cs new file mode 100644 index 0000000..a1fee8a --- /dev/null +++ b/Scripts/ShadowcasterUpdater.cs @@ -0,0 +1,35 @@ + +using System.Security.Permissions; +using UdonSharp; +using UnityEngine; +using VRC.SDKBase; +using VRC.Udon; + +public class ShadowcasterUpdater : UdonSharpBehaviour +{ + + public Renderer[] rendererTargets; + public string propertyName = "_Udon_WorldToLocal"; + private MaterialPropertyBlock _mpb; + + void Start() + { + _mpb = new MaterialPropertyBlock(); + } + + + void LateUpdate() + { + var w2l = transform.worldToLocalMatrix; + + + foreach (Renderer mat in rendererTargets) + { + if (mat == null) continue; + mat.GetPropertyBlock(_mpb); + _mpb.SetMatrix(propertyName, w2l); + mat.SetPropertyBlock(_mpb); + } + + } +} diff --git a/Scripts/ShadowcasterUpdater.cs.meta b/Scripts/ShadowcasterUpdater.cs.meta new file mode 100644 index 0000000..794e5aa --- /dev/null +++ b/Scripts/ShadowcasterUpdater.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 36c63f8382c2aad48b73fe4628db0f38 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Shader/BlendinShader.shader b/Shader/BlendinShader.shader index 68200cc..1de969c 100644 --- a/Shader/BlendinShader.shader +++ b/Shader/BlendinShader.shader @@ -17,6 +17,12 @@ Shader "DeMuenu/World/Hoppou/RevealStandart" _LightCutoffDistance ("Light Cutoff Distance", Float) = 100 //Moonlight END + _shadowCasterTex ("Shadow Caster Texture", 2D) = "white" {} + _shadowCasterColor ("Shadow Caster Color", Color) = (1,1,1,1) + _OutSideColor ("Outside Color", Color) = (1,1,1,1) + _MinBrightnessShadow ("Min Brightness for Shadows", Range(0,1)) = 0 + + } @@ -36,6 +42,7 @@ Shader "DeMuenu/World/Hoppou/RevealStandart" #include "Includes/Lambert.hlsl" #include "Includes/DefaultSetup.hlsl" #include "Includes/Variables.hlsl" + #include "Includes/Shadowcaster.cginc" //Moonlight Defines #define MAX_LIGHTS 80 // >= maxPlayers in script @@ -84,6 +91,12 @@ Shader "DeMuenu/World/Hoppou/RevealStandart" MoonlightGlobalVariables + float4x4 _Udon_WorldToLocal; + sampler2D _shadowCasterTex; + float4 _shadowCasterColor; + float4 _OutSideColor; + float _MinBrightnessShadow; + v2f vert (appdata v) { @@ -140,7 +153,15 @@ Shader "DeMuenu/World/Hoppou/RevealStandart" LightTypeCalculations(_Udon_LightColors, LightCounter, i, NdotL, dIntensity, _Udon_LightPositions[LightCounter].a, _Udon_LightPositions[LightCounter].xyz); - dmax = dmax + contrib * float4(LightColor, 1) * NdotL; // accumulate light contributions + float4 ShadowCasterMult = float4(1,1,1,1); + if (_Udon_ShadowMapIndex[LightCounter] > 0.5) { + ShadowCasterMult = SampleShadowcasterPlane(_Udon_WorldToLocal, _shadowCasterTex, _Udon_LightPositions[LightCounter].xyz, i.worldPos, _OutSideColor); + ShadowCasterMult *= _shadowCasterColor; + ShadowCasterMult = float4(ShadowCasterMult.rgb * (1-ShadowCasterMult.a), 1); + } + + + dmax = dmax + contrib * float4(LightColor, 1) * NdotL * max(ShadowCasterMult, _MinBrightnessShadow); } diff --git a/Shader/Includes/Shadowcaster.cginc b/Shader/Includes/Shadowcaster.cginc new file mode 100644 index 0000000..0b062da --- /dev/null +++ b/Shader/Includes/Shadowcaster.cginc @@ -0,0 +1,56 @@ +#ifndef SHADOWCASTER_PLANE +#define SHADOWCASTER_PLANE + +#include "UnityCG.cginc" // for tex2Dlod, etc. + +static const float EPS = 1e-5; + +// Returns whether segment hits the unit plane quad in plane-local z=0. +// Outputs uv in [0,1] and t in [0,1] along A->B. +inline bool RaySegmentHitsPlaneQuad(float4x4 worldToLocal, float3 rayOrigin, float3 rayEnd, out float2 uv, out float t) +{ + float3 aP = mul(worldToLocal, float4(rayOrigin, 1)).xyz; + float3 bP = mul(worldToLocal, float4(rayEnd, 1)).xyz; + + float3 d = bP - aP; + float dz = d.z; + + // Parallel-ish to plane? + if (abs(dz) < EPS) return false; + + // Intersect z=0 + t = -aP.z / dz; + + // Segment only + if (t < 0.0 || t > 1.0) return false; + + float3 hit = aP + d * t; + + // Inside 1x1 centered quad? + if (abs(hit.x) > 0.5 || abs(hit.y) > 0.5) return false; + + uv = hit.xy + 0.5; // [-0.5,0.5] -> [0,1] + return true; +} + +// Fragment-shader version: uses proper filtering/mips via tex2D +inline float4 SampleShadowcasterPlane(float4x4 worldToLocal, sampler2D tex, float3 rayOrigin, float3 rayEnd, float4 OutsideColor) +{ + float2 uv; float t; + if (RaySegmentHitsPlaneQuad(worldToLocal, rayOrigin, rayEnd, uv, t)) + return tex2D(tex, uv); // full color + + return OutsideColor; +} + +// Anywhere (vertex/geom/compute/custom code) version: forces LOD 0 +inline float4 SampleShadowcasterPlaneLOD0(float4x4 worldToLocal, sampler2D tex, float3 rayOrigin, float3 rayEnd, float4 OutsideColor) +{ + float2 uv; float t; + if (RaySegmentHitsPlaneQuad(worldToLocal, rayOrigin, rayEnd, uv, t)) + return tex2Dlod(tex, float4(uv, 0, 0)); // full color at mip 0 + + return OutsideColor; +} + +#endif diff --git a/Shader/Includes/Shadowcaster.cginc.meta b/Shader/Includes/Shadowcaster.cginc.meta new file mode 100644 index 0000000..cf0d55a --- /dev/null +++ b/Shader/Includes/Shadowcaster.cginc.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 407fd8c92bce1a84ab69e3abad2320b0 +ShaderIncludeImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Shader/Includes/Variables.hlsl b/Shader/Includes/Variables.hlsl index 665b19f..95e74a8 100644 --- a/Shader/Includes/Variables.hlsl +++ b/Shader/Includes/Variables.hlsl @@ -8,6 +8,7 @@ float4 _Udon_LightColors[MAX_LIGHTS]; /* xyz = position */ \ float4 _Udon_LightDirections[MAX_LIGHTS]; /* xyz = direction, w = cos(halfAngle) */ \ float _Udon_LightType[MAX_LIGHTS]; /* 0 = sphere, 1 = cone */ \ + float _Udon_ShadowMapIndex[MAX_LIGHTS];\ float _Udon_PlayerCount; /* set via SetFloat */ \ #endif \ No newline at end of file