From 11979b655000704a70502c2c9e8ae8a12130c71a Mon Sep 17 00:00:00 2001 From: freezy Date: Sun, 28 Jun 2026 22:33:44 +0200 Subject: [PATCH 1/7] editor: Move authoring adapters to new Editor assembly. Introduce a dedicated VisualPinball.Engine.Unity.Urp.Editor assembly and move MaterialAdapter and PrefabProvider into it. Their IMaterialAdapter and IPrefabProvider members are now exposed through a new UniversalRenderPipelineConverter (IRenderPipelineConverter) rather than the runtime UniversalRenderPipeline, keeping editor-only authoring concerns out of the runtime assembly. --- Editor.meta | 8 +++++ {Runtime => Editor}/MaterialAdapter.cs | 1 + {Runtime => Editor}/MaterialAdapter.cs.meta | 0 {Runtime => Editor}/PrefabProvider.cs | 1 + {Runtime => Editor}/PrefabProvider.cs.meta | 0 Editor/UniversalRenderPipelineConverter.cs | 35 +++++++++++++++++++ .../UniversalRenderPipelineConverter.cs.meta | 2 ++ ...sualPinball.Engine.Unity.Urp.Editor.asmdef | 23 ++++++++++++ ...inball.Engine.Unity.Urp.Editor.asmdef.meta | 7 ++++ Runtime/UniversalRenderPipeline.cs | 2 -- 10 files changed, 77 insertions(+), 2 deletions(-) create mode 100644 Editor.meta rename {Runtime => Editor}/MaterialAdapter.cs (95%) rename {Runtime => Editor}/MaterialAdapter.cs.meta (100%) rename {Runtime => Editor}/PrefabProvider.cs (96%) rename {Runtime => Editor}/PrefabProvider.cs.meta (100%) create mode 100644 Editor/UniversalRenderPipelineConverter.cs create mode 100644 Editor/UniversalRenderPipelineConverter.cs.meta create mode 100644 Editor/VisualPinball.Engine.Unity.Urp.Editor.asmdef create mode 100644 Editor/VisualPinball.Engine.Unity.Urp.Editor.asmdef.meta diff --git a/Editor.meta b/Editor.meta new file mode 100644 index 0000000..b2f21eb --- /dev/null +++ b/Editor.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: fbe02743bfd44074380f9269a2fd5dee +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/MaterialAdapter.cs b/Editor/MaterialAdapter.cs similarity index 95% rename from Runtime/MaterialAdapter.cs rename to Editor/MaterialAdapter.cs index 7467bf7..b4f201d 100644 --- a/Runtime/MaterialAdapter.cs +++ b/Editor/MaterialAdapter.cs @@ -19,6 +19,7 @@ using NLog; using UnityEngine; +using VisualPinball.Unity.Editor; using Logger = NLog.Logger; namespace VisualPinball.Unity.Urp diff --git a/Runtime/MaterialAdapter.cs.meta b/Editor/MaterialAdapter.cs.meta similarity index 100% rename from Runtime/MaterialAdapter.cs.meta rename to Editor/MaterialAdapter.cs.meta diff --git a/Runtime/PrefabProvider.cs b/Editor/PrefabProvider.cs similarity index 96% rename from Runtime/PrefabProvider.cs rename to Editor/PrefabProvider.cs index 16e2acb..4086e56 100644 --- a/Runtime/PrefabProvider.cs +++ b/Editor/PrefabProvider.cs @@ -17,6 +17,7 @@ using System; using UnityEngine; using VisualPinball.Engine.VPT; +using VisualPinball.Unity.Editor; namespace VisualPinball.Unity.Urp { diff --git a/Runtime/PrefabProvider.cs.meta b/Editor/PrefabProvider.cs.meta similarity index 100% rename from Runtime/PrefabProvider.cs.meta rename to Editor/PrefabProvider.cs.meta diff --git a/Editor/UniversalRenderPipelineConverter.cs b/Editor/UniversalRenderPipelineConverter.cs new file mode 100644 index 0000000..207865a --- /dev/null +++ b/Editor/UniversalRenderPipelineConverter.cs @@ -0,0 +1,35 @@ +// Visual Pinball Engine +// Copyright (C) 2020 freezy and VPE Team +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +// ReSharper disable UnusedType.Global +// ReSharper disable CheckNamespace + +using VisualPinball.Unity; +using VisualPinball.Unity.Editor; + +namespace VisualPinball.Unity.Urp +{ + public class UniversalRenderPipelineConverter : IRenderPipelineConverter + { + public const string PrefabPath = "Packages/org.visualpinball.engine.unity.urp/Prefabs"; + + public string Name => "Universal Render Pipeline"; + + public RenderPipelineType Type => RenderPipelineType.Urp; + public IMaterialAdapter MaterialAdapter { get; } = new MaterialAdapter(); + public IPrefabProvider PrefabProvider { get; } = new PrefabProvider(); + } +} diff --git a/Editor/UniversalRenderPipelineConverter.cs.meta b/Editor/UniversalRenderPipelineConverter.cs.meta new file mode 100644 index 0000000..bc39b44 --- /dev/null +++ b/Editor/UniversalRenderPipelineConverter.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: e0d87b6978d422940b5e4b9969424925 \ No newline at end of file diff --git a/Editor/VisualPinball.Engine.Unity.Urp.Editor.asmdef b/Editor/VisualPinball.Engine.Unity.Urp.Editor.asmdef new file mode 100644 index 0000000..e9c8ab9 --- /dev/null +++ b/Editor/VisualPinball.Engine.Unity.Urp.Editor.asmdef @@ -0,0 +1,23 @@ +{ + "name": "VisualPinball.Engine.Unity.Urp.Editor", + "rootNamespace": "VisualPinball.Unity.Urp", + "references": [ + "VisualPinball.Engine", + "VisualPinball.Engine.Editor", + "VisualPinball.Unity", + "VisualPinball.Unity.Patcher", + "VisualPinball.Unity.Editor", + "VisualPinball.Engine.Unity.Urp", + "Unity.RenderPipelines.Core.Runtime", + "Unity.RenderPipelines.Universal.Runtime" + ], + "includePlatforms": [ "Editor" ], + "excludePlatforms": [], + "allowUnsafeCode": true, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} diff --git a/Editor/VisualPinball.Engine.Unity.Urp.Editor.asmdef.meta b/Editor/VisualPinball.Engine.Unity.Urp.Editor.asmdef.meta new file mode 100644 index 0000000..b781e44 --- /dev/null +++ b/Editor/VisualPinball.Engine.Unity.Urp.Editor.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 903ad68751f6b2548b549db5a8af9246 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/UniversalRenderPipeline.cs b/Runtime/UniversalRenderPipeline.cs index 71347d3..e6f3492 100644 --- a/Runtime/UniversalRenderPipeline.cs +++ b/Runtime/UniversalRenderPipeline.cs @@ -27,9 +27,7 @@ public class UniversalRenderPipeline : IRenderPipeline public RenderPipelineType Type { get; } = RenderPipelineType.Urp; public IMaterialConverter MaterialConverter { get; } = new MaterialConverter(); - public IMaterialAdapter MaterialAdapter { get; } = new MaterialAdapter(); public ILightConverter LightConverter { get; } = new LightConverter(); public IBallConverter BallConverter { get; } = new BallConverter(); - public IPrefabProvider PrefabProvider { get; } = new PrefabProvider(); } } From e1d471a142cf14b18b19ca3e96904c316563a209 Mon Sep 17 00:00:00 2001 From: freezy Date: Sun, 28 Jun 2026 23:15:27 +0200 Subject: [PATCH 2/7] urp: Add material resolver for portable VPE materials. Turn portable VpeMaterialProfiles from a .vpe package into live URP/Lit materials in the Player, replacing glTFast placeholders. This is the URP counterpart of HdrpMaterialResolver: handle base color/map, metallic/smoothness, HDRP-style mask map split into URP metallic-gloss + occlusion, normal map, emissive, and surface/blend state. Shadergraph types and decals fall back to a plain lit build or are left to glTFast. Register the resolver before scene load via a bootstrap, mirroring the HDRP package. --- Runtime/UrpMaterialResolver.cs | 250 +++++++++++++++++++ Runtime/UrpMaterialResolver.cs.meta | 2 + Runtime/UrpMaterialResolverBootstrap.cs | 31 +++ Runtime/UrpMaterialResolverBootstrap.cs.meta | 2 + 4 files changed, 285 insertions(+) create mode 100644 Runtime/UrpMaterialResolver.cs create mode 100644 Runtime/UrpMaterialResolver.cs.meta create mode 100644 Runtime/UrpMaterialResolverBootstrap.cs create mode 100644 Runtime/UrpMaterialResolverBootstrap.cs.meta diff --git a/Runtime/UrpMaterialResolver.cs b/Runtime/UrpMaterialResolver.cs new file mode 100644 index 0000000..f12631b --- /dev/null +++ b/Runtime/UrpMaterialResolver.cs @@ -0,0 +1,250 @@ +// Visual Pinball Engine +// Copyright (C) 2026 freezy and VPE Team +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +// ReSharper disable CheckNamespace + +using UnityEngine; +using UnityEngine.Rendering; +using VisualPinball.Unity; + +namespace VisualPinball.Unity.Urp +{ + // Turns portable VpeMaterialProfiles (from a .vpe package's materials.json) into live + // URP/Lit materials in the Player, replacing the placeholder materials glTFast produces for + // the stripped table.glb. This is the URP counterpart of HdrpMaterialResolver. + // + // First pass: handles the portable vpe.lit core (base color/map, metallic/smoothness, + // HDRP-style mask map split into URP metallic-gloss + occlusion, normal map, emissive, + // surface/blend state, double-sided). Shadergraph types (metal/rubber/dmd) and decals fall + // back to a plain lit build from the profile's Lit data, or are left to glTFast when no Lit + // data exists. + public sealed class UrpMaterialResolver : IVpeMaterialResolver + { + private static Shader _litShader; + private static Shader LitShader => _litShader ? _litShader : (_litShader = Shader.Find("Universal Render Pipeline/Lit")); + + private static readonly int BaseColorId = Shader.PropertyToID("_BaseColor"); + private static readonly int BaseMapId = Shader.PropertyToID("_BaseMap"); + private static readonly int MetallicId = Shader.PropertyToID("_Metallic"); + private static readonly int SmoothnessId = Shader.PropertyToID("_Smoothness"); + private static readonly int MetallicGlossMapId = Shader.PropertyToID("_MetallicGlossMap"); + private static readonly int OcclusionMapId = Shader.PropertyToID("_OcclusionMap"); + private static readonly int OcclusionStrengthId = Shader.PropertyToID("_OcclusionStrength"); + private static readonly int BumpMapId = Shader.PropertyToID("_BumpMap"); + private static readonly int BumpScaleId = Shader.PropertyToID("_BumpScale"); + private static readonly int EmissionColorId = Shader.PropertyToID("_EmissionColor"); + private static readonly int EmissionMapId = Shader.PropertyToID("_EmissionMap"); + private static readonly int SurfaceId = Shader.PropertyToID("_Surface"); + private static readonly int BlendId = Shader.PropertyToID("_Blend"); + private static readonly int AlphaClipId = Shader.PropertyToID("_AlphaClip"); + private static readonly int CutoffId = Shader.PropertyToID("_Cutoff"); + private static readonly int SrcBlendId = Shader.PropertyToID("_SrcBlend"); + private static readonly int DstBlendId = Shader.PropertyToID("_DstBlend"); + private static readonly int ZWriteId = Shader.PropertyToID("_ZWrite"); + private static readonly int CullId = Shader.PropertyToID("_Cull"); + + public bool Supports(string materialType) + { + return materialType switch { + VpeMaterialTypes.Lit => true, + VpeMaterialTypes.Metal => true, + VpeMaterialTypes.Rubber => true, + VpeMaterialTypes.FabricSilk => true, + VpeMaterialTypes.Unlit => true, + _ => false, + }; + } + + public Material CreateMaterial(VpeMaterialProfile profile, IVpeTextureProvider textures, Material importedMaterial) + { + if (profile == null) { + return null; + } + + var lit = profile.Lit; + if (lit == null && profile.Fabric != null) { + lit = profile.Fabric.Lit; + } + if (lit == null) { + // No portable lit data (e.g. shadergraph-only or decal); keep glTFast's material. + return null; + } + + return BuildLit(profile.Name, lit, textures, importedMaterial); + } + + private static Material BuildLit(string name, VpeLitProfile lit, IVpeTextureProvider textures, Material imported) + { + if (lit == null || !LitShader) { + return null; + } + + var material = new Material(LitShader) { name = name, enableInstancing = true }; + + // --- base color + map --- + material.SetColor(BaseColorId, (Color)lit.BaseColor.Color); + var baseTex = ResolveTexture(lit.BaseColor.Texture, textures, imported, "_BaseMap"); + if (baseTex) { + material.SetTexture(BaseMapId, baseTex); + ApplyScaleOffset(material, BaseMapId, lit.BaseColor.Texture); + } + + // --- metallic / smoothness --- + material.SetFloat(MetallicId, lit.Metallic); + material.SetFloat(SmoothnessId, lit.Smoothness); + + // --- mask map --- + // HDRP MaskMap packs R=metallic, G=AO, B=detail, A=smoothness. URP/Lit reads metallic + // from R and smoothness from A of _MetallicGlossMap, and AO from G of _OcclusionMap, so + // the very same texture feeds both slots. + if (lit.MaskMap != null && !string.IsNullOrWhiteSpace(lit.MaskMap.TextureId) + && lit.MaskPacking == VpeMaskPackings.HdrpMaskMap) { + var mask = textures?.Get(lit.MaskMap.TextureId); + if (mask) { + material.SetTexture(MetallicGlossMapId, mask); + material.EnableKeyword("_METALLICSPECGLOSSMAP"); + material.SetTexture(OcclusionMapId, mask); + material.EnableKeyword("_OCCLUSIONMAP"); + material.SetFloat(OcclusionStrengthId, lit.OcclusionStrength); + } + } + + // --- normal map --- + // Source normals are plain RGB (alpha=1); URP's UnpackNormalmapRGorAG reads X from R*A, + // so a plain RGB normal samples correctly without the AG repack HDRP needs. + var normalTex = ResolveTexture(lit.NormalMap?.TextureId, textures, imported, "_BumpMap"); + if (normalTex) { + material.SetTexture(BumpMapId, normalTex); + material.SetFloat(BumpScaleId, lit.NormalMap.Strength); + material.EnableKeyword("_NORMALMAP"); + ApplyScaleOffset(material, BumpMapId, lit.NormalMap.Offset, lit.NormalMap.Scale); + } + + // --- emissive --- + var emissive = (Color)lit.Emissive.Color; + if (emissive.maxColorComponent > 0.0001f) { + material.SetColor(EmissionColorId, emissive); + var emissiveTex = ResolveTexture(lit.Emissive.Texture, textures, imported, "_EmissionMap"); + if (emissiveTex) { + material.SetTexture(EmissionMapId, emissiveTex); + } + material.EnableKeyword("_EMISSION"); + material.globalIlluminationFlags = MaterialGlobalIlluminationFlags.RealtimeEmissive; + } else { + material.globalIlluminationFlags = MaterialGlobalIlluminationFlags.EmissiveIsBlack; + } + + ApplySurface(material, lit); + + material.SetFloat(CullId, lit.DoubleSided ? (float)CullMode.Off : (float)CullMode.Back); + material.doubleSidedGI = lit.DoubleSidedGi; + + return material; + } + + private static void ApplySurface(Material material, VpeLitProfile lit) + { + switch (lit.SurfaceType) { + case VpeSurfaceTypes.Transparent: + material.SetFloat(SurfaceId, 1f); + material.SetFloat(AlphaClipId, 0f); + material.DisableKeyword("_ALPHATEST_ON"); + material.EnableKeyword("_SURFACE_TYPE_TRANSPARENT"); + material.SetFloat(ZWriteId, 0f); + material.SetOverrideTag("RenderType", "Transparent"); + material.renderQueue = (int)RenderQueue.Transparent; + ApplyBlend(material, lit.BlendMode); + break; + + case VpeSurfaceTypes.AlphaTest: + material.SetFloat(SurfaceId, 0f); + material.SetFloat(AlphaClipId, 1f); + material.SetFloat(CutoffId, lit.AlphaCutoff); + material.EnableKeyword("_ALPHATEST_ON"); + material.DisableKeyword("_SURFACE_TYPE_TRANSPARENT"); + material.SetFloat(SrcBlendId, (float)BlendMode.One); + material.SetFloat(DstBlendId, (float)BlendMode.Zero); + material.SetFloat(ZWriteId, 1f); + material.SetOverrideTag("RenderType", "TransparentCutout"); + material.renderQueue = (int)RenderQueue.AlphaTest; + break; + + default: // Opaque + material.SetFloat(SurfaceId, 0f); + material.SetFloat(AlphaClipId, 0f); + material.DisableKeyword("_ALPHATEST_ON"); + material.DisableKeyword("_SURFACE_TYPE_TRANSPARENT"); + material.SetFloat(SrcBlendId, (float)BlendMode.One); + material.SetFloat(DstBlendId, (float)BlendMode.Zero); + material.SetFloat(ZWriteId, 1f); + material.SetOverrideTag("RenderType", "Opaque"); + material.renderQueue = (int)RenderQueue.Geometry; + break; + } + } + + private static void ApplyBlend(Material material, string blendMode) + { + switch (blendMode) { + case VpeBlendModes.Additive: + material.SetFloat(BlendId, 2f); + material.SetFloat(SrcBlendId, (float)BlendMode.SrcAlpha); + material.SetFloat(DstBlendId, (float)BlendMode.One); + break; + case VpeBlendModes.Premultiply: + material.SetFloat(BlendId, 1f); + material.SetFloat(SrcBlendId, (float)BlendMode.One); + material.SetFloat(DstBlendId, (float)BlendMode.OneMinusSrcAlpha); + break; + default: // Alpha + material.SetFloat(BlendId, 0f); + material.SetFloat(SrcBlendId, (float)BlendMode.SrcAlpha); + material.SetFloat(DstBlendId, (float)BlendMode.OneMinusSrcAlpha); + break; + } + } + + private static Texture ResolveTexture(VpeTextureRef textureRef, IVpeTextureProvider textures, Material imported, string importedProperty) + { + return ResolveTexture(textureRef?.TextureId, textures, imported, importedProperty); + } + + private static Texture ResolveTexture(string textureId, IVpeTextureProvider textures, Material imported, string importedProperty) + { + Texture texture = null; + if (!string.IsNullOrWhiteSpace(textureId)) { + texture = textures?.Get(textureId); + } + if (!texture && imported && !string.IsNullOrEmpty(importedProperty) && imported.HasProperty(importedProperty)) { + texture = imported.GetTexture(importedProperty); + } + return texture; + } + + private static void ApplyScaleOffset(Material material, int propertyId, VpeTextureRef textureRef) + { + if (textureRef != null) { + ApplyScaleOffset(material, propertyId, textureRef.Offset, textureRef.Scale); + } + } + + private static void ApplyScaleOffset(Material material, int propertyId, Vector2 offset, Vector2 scale) + { + material.SetTextureOffset(propertyId, offset); + material.SetTextureScale(propertyId, scale); + } + } +} diff --git a/Runtime/UrpMaterialResolver.cs.meta b/Runtime/UrpMaterialResolver.cs.meta new file mode 100644 index 0000000..2b89568 --- /dev/null +++ b/Runtime/UrpMaterialResolver.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 0b457a29ac3c5134091a2776fee381ba \ No newline at end of file diff --git a/Runtime/UrpMaterialResolverBootstrap.cs b/Runtime/UrpMaterialResolverBootstrap.cs new file mode 100644 index 0000000..b6a23a5 --- /dev/null +++ b/Runtime/UrpMaterialResolverBootstrap.cs @@ -0,0 +1,31 @@ +// Visual Pinball Engine +// Copyright (C) 2026 freezy and VPE Team +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +// ReSharper disable CheckNamespace + +using UnityEngine; +using VisualPinball.Unity; + +namespace VisualPinball.Unity.Urp +{ + // Registers the URP material resolver with VpeMaterialResolver before any scene loads, the + // same pattern as the HDRP package's VpeMaterialResolverBootstrap. + public static class UrpMaterialResolverBootstrap + { + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] + private static void Register() => VpeMaterialResolver.Register(new UrpMaterialResolver()); + } +} diff --git a/Runtime/UrpMaterialResolverBootstrap.cs.meta b/Runtime/UrpMaterialResolverBootstrap.cs.meta new file mode 100644 index 0000000..b7638cb --- /dev/null +++ b/Runtime/UrpMaterialResolverBootstrap.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 5648d6e0030b6374eba79caf60aa18f2 \ No newline at end of file From d3382c68f83f7824d05aa85462eb0a2b4461f8a2 Mon Sep 17 00:00:00 2001 From: freezy Date: Mon, 29 Jun 2026 00:27:08 +0200 Subject: [PATCH 3/7] urp: Fix smoothness scaling when occlusion mask is present. URP computes smoothness as mask.a * _Smoothness, while HDRP uses lerp(remapMin, remapMax, mask.a) and ignores the scalar when a mask is set. Set _Smoothness to the remap max so mask.a drives smoothness fully instead of being scaled down by the mask-irrelevant scalar. --- Runtime/UrpMaterialResolver.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Runtime/UrpMaterialResolver.cs b/Runtime/UrpMaterialResolver.cs index f12631b..5428266 100644 --- a/Runtime/UrpMaterialResolver.cs +++ b/Runtime/UrpMaterialResolver.cs @@ -119,6 +119,10 @@ private static Material BuildLit(string name, VpeLitProfile lit, IVpeTextureProv material.SetTexture(OcclusionMapId, mask); material.EnableKeyword("_OCCLUSIONMAP"); material.SetFloat(OcclusionStrengthId, lit.OcclusionStrength); + // URP computes smoothness = mask.a * _Smoothness; HDRP uses lerp(remapMin,remapMax,mask.a) + // and ignores the scalar when a mask is present. Use the remap max so mask.a drives + // smoothness fully instead of being scaled down by the (mask-irrelevant) scalar. + material.SetFloat(SmoothnessId, lit.SmoothnessRemap.y); } } From 686de2f9dfef9ce47314581a963472563fb9d642 Mon Sep 17 00:00:00 2001 From: freezy Date: Thu, 2 Jul 2026 13:42:00 +0200 Subject: [PATCH 4/7] urp: Add insert materials and physical light unit conversion. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Map authored HDRP-physical values into URP's unitless lighting via a single ReferenceEv100 scale (UrpPhysicalUnits): emissives from nits and punctual/directional lights from candela/lux, the latter registered as a VpeLightUnitAdjuster at bootstrap. Implement emissive color/intensity on the URP MaterialConverter using _EmissionColor (chroma × intensity), replacing the earlier no-op stubs so LightComponent can drive faux bulbs and inserts. Add insert material support: transmittance/base color becomes the body color and the lamp glow becomes emission driven at runtime, approximating HDRP translucency that URP lacks. --- Runtime/MaterialConverter.cs | 33 +++++++--- Runtime/UrpMaterialResolver.cs | 81 ++++++++++++++++++++++++- Runtime/UrpMaterialResolverBootstrap.cs | 9 ++- Runtime/UrpPhysicalUnits.cs | 54 +++++++++++++++++ Runtime/UrpPhysicalUnits.cs.meta | 2 + 5 files changed, 168 insertions(+), 11 deletions(-) create mode 100644 Runtime/UrpPhysicalUnits.cs create mode 100644 Runtime/UrpPhysicalUnits.cs.meta diff --git a/Runtime/MaterialConverter.cs b/Runtime/MaterialConverter.cs index e44b85e..224a2ca 100644 --- a/Runtime/MaterialConverter.cs +++ b/Runtime/MaterialConverter.cs @@ -155,28 +155,45 @@ public void SetMaterialType(Material material, MaterialType materialType) // todo } + // URP/Lit has a single HDR _EmissionColor instead of HDRP's LDR-color + intensity split. + // The equivalent semantics used here: intensity = max component of the material's emission + // color, chroma = emission color normalized by that maximum. GetEmissiveIntensity therefore + // returns the authored full-on intensity (the baseline LightComponent remembers), and + // SetEmissiveIntensity rebuilds chroma × intensity into the property block. + + private static readonly int EmissionColor = Shader.PropertyToID("_EmissionColor"); + public void SetEmissiveColor(MaterialPropertyBlock propBlock, Color color) { - // urp has no emissive color + propBlock.SetColor(EmissionColor, color); } public Color? GetEmissiveColor(Material material) { - // urp has no emissive color - - return null; + if (!material || !material.HasProperty(EmissionColor)) { + return null; + } + return material.GetColor(EmissionColor); } public void SetEmissiveIntensity(Material material, MaterialPropertyBlock propBlock, float intensity) { - // urp has no emissive intensity + if (!material || !material.HasProperty(EmissionColor)) { + return; + } + var baseline = material.GetColor(EmissionColor); + var max = baseline.maxColorComponent; + var chroma = max > 0f ? baseline / max : Color.white; + chroma.a = 1f; + propBlock.SetColor(EmissionColor, chroma * intensity); } public float GetEmissiveIntensity(Material material) { - // urp has no emissive intensity - // - return 0; + if (!material || !material.HasProperty(EmissionColor) || !material.IsKeywordEnabled("_EMISSION")) { + return 0; + } + return material.GetColor(EmissionColor).maxColorComponent; } } } diff --git a/Runtime/UrpMaterialResolver.cs b/Runtime/UrpMaterialResolver.cs index 5428266..b5c2333 100644 --- a/Runtime/UrpMaterialResolver.cs +++ b/Runtime/UrpMaterialResolver.cs @@ -60,6 +60,7 @@ public bool Supports(string materialType) { return materialType switch { VpeMaterialTypes.Lit => true, + VpeMaterialTypes.Insert => true, VpeMaterialTypes.Metal => true, VpeMaterialTypes.Rubber => true, VpeMaterialTypes.FabricSilk => true, @@ -74,6 +75,10 @@ public Material CreateMaterial(VpeMaterialProfile profile, IVpeTextureProvider t return null; } + if (profile.Type == VpeMaterialTypes.Insert) { + return BuildInsert(profile, textures, importedMaterial); + } + var lit = profile.Lit; if (lit == null && profile.Fabric != null) { lit = profile.Fabric.Lit; @@ -86,6 +91,77 @@ public Material CreateMaterial(VpeMaterialProfile profile, IVpeTextureProvider t return BuildLit(profile.Name, lit, textures, importedMaterial); } + #region Inserts + + // URP has no translucency, diffusion profiles or refraction, so an insert cannot be lit + // "through" its plastic the way HDRP does it. The approximation: the transmittance color + // (the physical color of the insert plastic) becomes the visible body color, and the lamp + // glow becomes emission that LightComponent drives with the lamp value at runtime — the + // same seam that drives faux bulbs, via MaterialConverter.Get/SetEmissiveIntensity. + // + // Both parts stay in the transparent queue: LightComponent hides *opaque* emissive + // renderers when the lamp is off (faux bulbs), while transparent ones only get their + // emission zeroed and stay visible — which is exactly what an insert needs. + + // Opacity floors. Authored base alpha is ~0.1 (clear plastic relying on transmission for + // its color); URP shows the color through the base map instead. + private const float ReflectorOpacity = 0.9f; + private const float LensOpacity = 0.4f; + + // Emission at full lamp intensity, in URP emission units (post-ReferenceEv100 scale). + // Standard alpha blending scales emission by the body alpha. Kept low enough that the + // tonemapper preserves the insert's chroma (over-driving it washes lit inserts to white). + private const float ReflectorGlow = 4f; + private const float LensGlow = 6f; + + private static Material BuildInsert(VpeMaterialProfile profile, IVpeTextureProvider textures, Material imported) + { + var insert = profile.Insert; + var lit = insert?.Lit; + if (lit == null) { + return null; + } + + var material = BuildLit(profile.Name, lit, textures, imported); + if (!material) { + return null; + } + + var isReflector = insert.Part == VpeInsertParts.Reflector; + + // Color identity: a reflector's most saturated color is its transmittance tint (the + // color it gives transmitted lamp light); a lens carries its color in the authored + // base color — its transmittance is typically a generic hat value shared across colors. + var tint = isReflector ? (Color)lit.TransmittanceColor : (Color)lit.BaseColor.Color; + tint.a = 1f; + var baseColor = (Color)lit.BaseColor.Color; + var bodyColor = Color.Lerp(baseColor, tint, 0.85f); + bodyColor.a = Mathf.Max(baseColor.a, isReflector ? ReflectorOpacity : LensOpacity); + material.SetColor(BaseColorId, bodyColor); + + // The authored blend mode may be premultiply; URP/Lit expects the premultiply keyword + // for that to look right, so force plain alpha blending — with the boosted opacity + // above it reads much closer to the HDRP lens anyway. + material.SetFloat(BlendId, 0f); + material.SetFloat(SrcBlendId, (float)BlendMode.SrcAlpha); + material.SetFloat(DstBlendId, (float)BlendMode.OneMinusSrcAlpha); + + // Emission baseline: chroma from the tint, magnitude = full-lamp glow. LightComponent + // reads this baseline at Awake, zeroes it, and scales it with the lamp value. + var chroma = tint.maxColorComponent > 0f ? tint / tint.maxColorComponent : Color.white; + chroma.a = 1f; + material.SetColor(EmissionColorId, chroma * (isReflector ? ReflectorGlow : LensGlow)); + material.EnableKeyword("_EMISSION"); + material.globalIlluminationFlags = MaterialGlobalIlluminationFlags.RealtimeEmissive; + + // Keep the lens above the reflector within the transparent queue. + material.renderQueue = (int)RenderQueue.Transparent + lit.SortPriority; + + return material; + } + + #endregion + private static Material BuildLit(string name, VpeLitProfile lit, IVpeTextureProvider textures, Material imported) { if (lit == null || !LitShader) { @@ -138,7 +214,10 @@ private static Material BuildLit(string name, VpeLitProfile lit, IVpeTextureProv } // --- emissive --- - var emissive = (Color)lit.Emissive.Color; + // Authored emissive colors are HDRP-physical (nits); map them into URP's unitless + // emission through the shared reference-exposure scale. + var emissive = (Color)lit.Emissive.Color * UrpPhysicalUnits.EmissiveScale; + emissive.a = 1f; if (emissive.maxColorComponent > 0.0001f) { material.SetColor(EmissionColorId, emissive); var emissiveTex = ResolveTexture(lit.Emissive.Texture, textures, imported, "_EmissionMap"); diff --git a/Runtime/UrpMaterialResolverBootstrap.cs b/Runtime/UrpMaterialResolverBootstrap.cs index b6a23a5..8b00f06 100644 --- a/Runtime/UrpMaterialResolverBootstrap.cs +++ b/Runtime/UrpMaterialResolverBootstrap.cs @@ -22,10 +22,15 @@ namespace VisualPinball.Unity.Urp { // Registers the URP material resolver with VpeMaterialResolver before any scene loads, the - // same pattern as the HDRP package's VpeMaterialResolverBootstrap. + // same pattern as the HDRP package's VpeMaterialResolverBootstrap. Also registers the light + // unit adjuster that converts authored HDRP-physical light intensities into URP's units. public static class UrpMaterialResolverBootstrap { [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] - private static void Register() => VpeMaterialResolver.Register(new UrpMaterialResolver()); + private static void Register() + { + VpeMaterialResolver.Register(new UrpMaterialResolver()); + VpeLightUnitAdjuster.Register(UrpPhysicalUnits.AdjustRestoredLight); + } } } diff --git a/Runtime/UrpPhysicalUnits.cs b/Runtime/UrpPhysicalUnits.cs new file mode 100644 index 0000000..bcb4436 --- /dev/null +++ b/Runtime/UrpPhysicalUnits.cs @@ -0,0 +1,54 @@ +// Visual Pinball Engine +// Copyright (C) 2026 freezy and VPE Team +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +// ReSharper disable CheckNamespace + +using UnityEngine; + +namespace VisualPinball.Unity.Urp +{ + // .vpe tables are authored under HDRP in physical units: emissives in nits, punctual lights + // in candela (Light.intensity holds the candela value), directionals in lux. HDRP tone-maps + // those through exposure (signal = luminance / (1.2 * 2^EV100)); URP has neither physical + // units nor an exposure system, so this is the single place where physical values are mapped + // into URP's unitless lighting. + public static class UrpPhysicalUnits + { + // Reference exposure baked into the conversion. Calibrated against the HDRP-rendered + // lights-on package screenshots (T2): around EV 3–4, faux bulbs read blown-white with + // bloom (as in HDRP) and GI lamps light the playfield warmly without washing the paint + // out. Tune this one value if overall URP brightness drifts from the HDRP player. + public const float ReferenceEv100 = 3.5f; + + // nits (emissive luminance) → URP emission value. + public static readonly float EmissiveScale = 1f / (1.2f * Mathf.Pow(2f, ReferenceEv100)); + + // candela / lux → URP light intensity. URP's Lambert term carries no 1/π (the legacy + // Unity convention bakes it into the light), so physical light intensities additionally + // divide by π. + public static readonly float LightScale = EmissiveScale / Mathf.PI; + + // Registered with VpeLightUnitAdjuster at bootstrap; runs after each restored light + // profile has been applied with its authored (HDRP-physical) values. + public static void AdjustRestoredLight(Light light) + { + if (!light) { + return; + } + light.intensity *= LightScale; + } + } +} diff --git a/Runtime/UrpPhysicalUnits.cs.meta b/Runtime/UrpPhysicalUnits.cs.meta new file mode 100644 index 0000000..14d65af --- /dev/null +++ b/Runtime/UrpPhysicalUnits.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 14342a43133dd4b4e954db52ae4bf883 \ No newline at end of file From 4cc48b0367e73c28ce47a1130132a48b2ae7aef8 Mon Sep 17 00:00:00 2001 From: freezy Date: Thu, 2 Jul 2026 21:26:28 +0200 Subject: [PATCH 5/7] Add decal material support to URP resolver Handle VpeMaterialTypes.Decal by rendering the decal mesh as an alpha-blended lit overlay, since URP's decal system only supports projectors. The base-map alpha drives where the decal applies, with HDRP's Decal Blend folded into the base alpha and mask/normal map reuse mirroring BuildLit. --- Runtime/UrpMaterialResolver.cs | 76 ++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/Runtime/UrpMaterialResolver.cs b/Runtime/UrpMaterialResolver.cs index b5c2333..c68b635 100644 --- a/Runtime/UrpMaterialResolver.cs +++ b/Runtime/UrpMaterialResolver.cs @@ -61,6 +61,7 @@ public bool Supports(string materialType) return materialType switch { VpeMaterialTypes.Lit => true, VpeMaterialTypes.Insert => true, + VpeMaterialTypes.Decal => true, VpeMaterialTypes.Metal => true, VpeMaterialTypes.Rubber => true, VpeMaterialTypes.FabricSilk => true, @@ -78,6 +79,9 @@ public Material CreateMaterial(VpeMaterialProfile profile, IVpeTextureProvider t if (profile.Type == VpeMaterialTypes.Insert) { return BuildInsert(profile, textures, importedMaterial); } + if (profile.Type == VpeMaterialTypes.Decal) { + return BuildDecal(profile, textures, importedMaterial); + } var lit = profile.Lit; if (lit == null && profile.Fabric != null) { @@ -91,6 +95,78 @@ public Material CreateMaterial(VpeMaterialProfile profile, IVpeTextureProvider t return BuildLit(profile.Name, lit, textures, importedMaterial); } + #region Decals + + // vpe.decal is authored as a decal *mesh* in VPE tables: dedicated geometry floating a + // hair above the surface it decorates, with the albedo alpha controlling where the decal + // applies. URP's decal system only handles projectors, so the URP realization renders the + // same mesh with an alpha-blended lit overlay built from the decal inputs. In HDRP the + // glb fallback for these meshes is a texture-free HDRP/Decal clone (invisible under URP), + // which is why unsupported decals show nothing rather than something white. + private static Material BuildDecal(VpeMaterialProfile profile, IVpeTextureProvider textures, Material imported) + { + var decal = profile.Decal; + if (decal == null || !LitShader) { + return null; + } + + var material = new Material(LitShader) { name = profile.Name, enableInstancing = true }; + + var baseColor = (Color)decal.BaseColor.Color; + // HDRP's Decal Blend scales the whole decal's opacity; fold it into the base alpha. + baseColor.a *= Mathf.Clamp01(decal.DecalBlend); + material.SetColor(BaseColorId, baseColor); + var baseTex = ResolveTexture(decal.BaseColor.Texture, textures, imported, "_BaseMap"); + if (baseTex) { + material.SetTexture(BaseMapId, baseTex); + ApplyScaleOffset(material, BaseMapId, decal.BaseColor.Texture); + } + + material.SetFloat(MetallicId, decal.Metallic); + material.SetFloat(SmoothnessId, decal.Smoothness); + + // Same HDRP mask-map reuse as BuildLit: R=metallic/A=smoothness feed _MetallicGlossMap, + // G=AO feeds _OcclusionMap. + if (decal.AffectMask && decal.MaskMap != null && !string.IsNullOrWhiteSpace(decal.MaskMap.TextureId) + && decal.MaskPacking == VpeMaskPackings.HdrpMaskMap) { + var mask = textures?.Get(decal.MaskMap.TextureId); + if (mask) { + material.SetTexture(MetallicGlossMapId, mask); + material.EnableKeyword("_METALLICSPECGLOSSMAP"); + material.SetTexture(OcclusionMapId, mask); + material.EnableKeyword("_OCCLUSIONMAP"); + material.SetFloat(OcclusionStrengthId, decal.AmbientOcclusion); + } + } + + var normalTex = decal.AffectNormal + ? ResolveTexture(decal.NormalMap?.TextureId, textures, imported, "_BumpMap") + : null; + if (normalTex) { + material.SetTexture(BumpMapId, normalTex); + material.SetFloat(BumpScaleId, decal.NormalMap.Strength); + material.EnableKeyword("_NORMALMAP"); + ApplyScaleOffset(material, BumpMapId, decal.NormalMap.Offset, decal.NormalMap.Scale); + } + + // Transparent alpha overlay: the base-map alpha decides where the decal applies. + material.SetFloat(SurfaceId, 1f); + material.SetFloat(AlphaClipId, 0f); + material.DisableKeyword("_ALPHATEST_ON"); + material.EnableKeyword("_SURFACE_TYPE_TRANSPARENT"); + material.SetFloat(BlendId, 0f); + material.SetFloat(SrcBlendId, (float)BlendMode.SrcAlpha); + material.SetFloat(DstBlendId, (float)BlendMode.OneMinusSrcAlpha); + material.SetFloat(ZWriteId, 0f); + material.SetOverrideTag("RenderType", "Transparent"); + material.renderQueue = (int)RenderQueue.Transparent; + material.globalIlluminationFlags = MaterialGlobalIlluminationFlags.EmissiveIsBlack; + + return material; + } + + #endregion + #region Inserts // URP has no translucency, diffusion profiles or refraction, so an insert cannot be lit From 4853103dd308011f6f6e535bb64905a14a182345 Mon Sep 17 00:00:00 2001 From: freezy Date: Thu, 2 Jul 2026 23:22:27 +0200 Subject: [PATCH 6/7] urp: Recalibrate lighting exposure and widen lamp ranges. Bump ReferenceEv100 to 5 (A/B'd against the HDRP player under a neutral HDRI) and add a separate EmissiveExtraEv stop so bloom-hot faux bulbs match HDRP while GI lamps stay in line. LightScale now derives from ReferenceEv100 alone, excluding the emissive-only stop. Add LightRangeMultiplier to widen point/spot ranges, letting the overlapping direct pools stand in for HDRP's missing SSGI bounce while still following lamp state. --- Runtime/UrpPhysicalUnits.cs | 31 +++++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/Runtime/UrpPhysicalUnits.cs b/Runtime/UrpPhysicalUnits.cs index bcb4436..575b348 100644 --- a/Runtime/UrpPhysicalUnits.cs +++ b/Runtime/UrpPhysicalUnits.cs @@ -27,19 +27,31 @@ namespace VisualPinball.Unity.Urp // into URP's unitless lighting. public static class UrpPhysicalUnits { - // Reference exposure baked into the conversion. Calibrated against the HDRP-rendered - // lights-on package screenshots (T2): around EV 3–4, faux bulbs read blown-white with - // bloom (as in HDRP) and GI lamps light the playfield warmly without washing the paint - // out. Tune this one value if overall URP brightness drifts from the HDRP player. - public const float ReferenceEv100 = 3.5f; + // Reference exposure baked into the conversion. Calibrated 2026-07-02 by A/B region-stats + // against the HDRP player under the neutral wooden_studio_17_1k HDRI (same camera pose, + // only table lights on): EV 4.5 brings GI-lamp pools and faux-bulb cores in line with + // HDRP's histogram exposure. Tune this one value if overall URP lamp/emissive brightness + // drifts from the HDRP player. + public const float ReferenceEv100 = 5f; + + // Extra stops applied to emissive surfaces on top of ReferenceEv100. A/B stats showed + // faux-bulb cores/halos still ~1 EV hotter than HDRP when lights already matched — + // URP's bloom picks emissive HDR values up more aggressively than HDRP's. + public const float EmissiveExtraEv = 1f; // nits (emissive luminance) → URP emission value. - public static readonly float EmissiveScale = 1f / (1.2f * Mathf.Pow(2f, ReferenceEv100)); + public static readonly float EmissiveScale = 1f / (1.2f * Mathf.Pow(2f, ReferenceEv100 + EmissiveExtraEv)); // candela / lux → URP light intensity. URP's Lambert term carries no 1/π (the legacy // Unity convention bakes it into the light), so physical light intensities additionally - // divide by π. - public static readonly float LightScale = EmissiveScale / Mathf.PI; + // divide by π. Derived from ReferenceEv100 alone — EmissiveExtraEv does not apply here. + public static readonly float LightScale = 1f / (1.2f * Mathf.Pow(2f, ReferenceEv100) * Mathf.PI); + + // HDRP spreads each lamp's light through SSGI (screen-space bounce); URP has no GI, so + // table lamps only produce their direct pools. Widening the authored ranges makes the + // overlapping pools stand in for the first bounce — and unlike a static ambient lift it + // follows the lamp state (GI string off → playfield goes dark, as in HDRP). + public const float LightRangeMultiplier = 3.5f; // Registered with VpeLightUnitAdjuster at bootstrap; runs after each restored light // profile has been applied with its authored (HDRP-physical) values. @@ -49,6 +61,9 @@ public static void AdjustRestoredLight(Light light) return; } light.intensity *= LightScale; + if (light.type is LightType.Point or LightType.Spot) { + light.range *= LightRangeMultiplier; + } } } } From 4e86982a5565bd0e4f68b875ff148153e2581abb Mon Sep 17 00:00:00 2001 From: freezy Date: Mon, 14 Sep 2026 00:34:01 +0200 Subject: [PATCH 7/7] urp: use correctly sized default ball mesh --- Assets/Resources/Prefabs/Trough.prefab | 7 +++++++ .../Prefabs/{DefaultBall.prefab => UrpBall.prefab} | 4 ++-- .../{DefaultBall.prefab.meta => UrpBall.prefab.meta} | 0 Runtime/BallConverter.cs | 2 +- 4 files changed, 10 insertions(+), 3 deletions(-) rename Assets/Resources/Prefabs/{DefaultBall.prefab => UrpBall.prefab} (97%) rename Assets/Resources/Prefabs/{DefaultBall.prefab.meta => UrpBall.prefab.meta} (100%) diff --git a/Assets/Resources/Prefabs/Trough.prefab b/Assets/Resources/Prefabs/Trough.prefab index b4fece6..8939103 100644 --- a/Assets/Resources/Prefabs/Trough.prefab +++ b/Assets/Resources/Prefabs/Trough.prefab @@ -56,6 +56,13 @@ MonoBehaviour: PlayfieldExitKickerItem: Ball: {fileID: 0} BallCount: 6 + BallPrefabs: + - {fileID: 0} + - {fileID: 0} + - {fileID: 0} + - {fileID: 0} + - {fileID: 0} + - {fileID: 0} SwitchCount: 6 JamSwitch: 0 RollTime: 300 diff --git a/Assets/Resources/Prefabs/DefaultBall.prefab b/Assets/Resources/Prefabs/UrpBall.prefab similarity index 97% rename from Assets/Resources/Prefabs/DefaultBall.prefab rename to Assets/Resources/Prefabs/UrpBall.prefab index 0cffa06..01a01ec 100644 --- a/Assets/Resources/Prefabs/DefaultBall.prefab +++ b/Assets/Resources/Prefabs/UrpBall.prefab @@ -11,7 +11,7 @@ GameObject: - component: {fileID: 8928362396421481961} - component: {fileID: 1782985097271273431} m_Layer: 0 - m_Name: DefaultBall + m_Name: UrpBall m_TagString: Untagged m_Icon: {fileID: 0} m_NavMeshLayer: 0 @@ -92,7 +92,7 @@ MeshFilter: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 8440806518127559228} - m_Mesh: {fileID: 10207, guid: 0000000000000000e000000000000000, type: 0} + m_Mesh: {fileID: 4983844940297228651, guid: d78bc7cfea69c684c8c631e82dd9e206, type: 3} --- !u!23 &7632225475359270604 MeshRenderer: m_ObjectHideFlags: 0 diff --git a/Assets/Resources/Prefabs/DefaultBall.prefab.meta b/Assets/Resources/Prefabs/UrpBall.prefab.meta similarity index 100% rename from Assets/Resources/Prefabs/DefaultBall.prefab.meta rename to Assets/Resources/Prefabs/UrpBall.prefab.meta diff --git a/Runtime/BallConverter.cs b/Runtime/BallConverter.cs index 3df509a..cd2f05e 100644 --- a/Runtime/BallConverter.cs +++ b/Runtime/BallConverter.cs @@ -25,7 +25,7 @@ public class BallConverter : IBallConverter { public GameObject CreateDefaultBall() { - return UnityEngine.Resources.Load("Prefabs/DefaultBall"); + return UnityEngine.Resources.Load("Prefabs/UrpBall"); } } }