【Unity】RenderFeature笔记

【Unity】RenderFeature笔记

RenderFeature是在urp中添加的额外渲染pass,并可以将这个pass插入到渲染列队中的任意位置。内置渲染管线中Graphics 的功能需要在RenderFeature里实现,常见的如DrawMesh和Blit

​ 可以实现的效果包括但不限于

  1. 后处理,可以编写shader对整个渲染画面进行修改。比如一些全局特效包括:场景描边,地形扫描;

  2. 创建网格,通过DrawMesh绘制定义的网格;

  3. 渲染画面,将场景渲染到纹理;

  4. ...

一、RanderFeature简介

1. 创建RanderFeature

在project中Create→Rendering→URPRenderFeature

2.RanderFeature结构
c# 复制代码
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;

public class CustomRenderPassFeatureTest : ScriptableRendererFeature
{
    class CustomRenderPass : ScriptableRenderPass
    {
        // 在执行渲染通道之前调用
        // 创建临时渲染目标纹理。
        //为Execute提前准备需要的RenderTexture或者其它变量
        public override void OnCameraSetup(CommandBuffer cmd, ref RenderingData renderingData){}
        //核心方法,实现这个renderPass的逻辑
        public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData){}
        //释放在OnCameraSetup里,声明的变量如TemporaryRenderTexture
        public override void OnCameraCleanup(CommandBuffer cmd) {}
    }
    CustomRenderPass m_ScriptablePass;

    /// <inheritdoc/>
    public override void Create()
    {
        m_ScriptablePass = new CustomRenderPass();
        //定义渲染的位置
        m_ScriptablePass.renderPassEvent = RenderPassEvent.AfterRenderingOpaques;
    }

    // 这里你可以在渲染器中注入一个或多个渲染通道。
    //每一帧都执行
    public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
    {
        //把实例化的CustomRenderPass插入渲染管线
        renderer.EnqueuePass(m_ScriptablePass);
    }
}
3.RenderFeatrue使用

在Universal Render Pipeline Asset_Renderer直接Add RenderFeatrue

二、后处理效果

关键词
1、CommandBuffer.Blit

Blit是很常用的后期效果方法,在Build_in中常常是在OnRenderImage里被调用,与urp不同的是,在build_in里是通过Graphics.Blit调用。

Graphics.Blit

常用的接口: Blit(RenderTexturesource, RenderTexture dest, Material mat);

即使用着色器将源纹理复制到目标渲染纹理。

CommandBuffer.Blit

常用的接口:public void Blit (Rendering.RenderTargetIdentifier source , Rendering.RenderTargetIdentifier dest , Material mat);

这与 Graphics.Blit相似 - 主要用于从一个(渲染)纹理复制到其他纹理,可能使用自定义着色器;

源纹理或渲染目标将作为"_MainTex"属性传递给材质。

在shader中,_MainTex对应的是source,最后retrun对应的是dest

2、RenderTargetIdentifier

渲染目标标识符,标识 CommandBuffer的 RenderTexture;

使用 CommandBuffer.GetTemporaryRT 创建的具有名称的临时渲染纹理;

ScriptableRenderPass中有colorAttachment和depthAttachment是可以直接使用;

RenderTargetHandle.CameraTarget.Identifier()可以得到当前的渲染标识符

renderingData.cameraData.renderer.cameraColorTarget 也可以得到当前的渲染标识符

简单实现

实现脚本

c# 复制代码
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;

public class CustomRenderPassFeatureTest : ScriptableRendererFeature
{
    //结构说明
    class CustomRenderPass : ScriptableRenderPass
    {
        // 在执行渲染通道之前调用
        // 创建临时渲染目标纹理。
        //为Execute提前准备需要的RenderTexture或者其它变量

        public override void OnCameraSetup(CommandBuffer cmd, ref RenderingData renderingData) {}
      
        //核心方法,实现这个renderPass的逻辑
        public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData){}

        //释放在OnCameraSetup里,声明的变量如TemporaryRenderTexture
        public override void OnCameraCleanup(CommandBuffer cmd){}
    }
    //后处理效果
    class CustomRenderShaderPostPass : ScriptableRenderPass
    {  
        //定义渲染材质,通过Create方法赋值
        public Material _Material;
        public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
        {
            //创建一个CommandBuffer
            CommandBuffer cmd = CommandBufferPool.Get("ShowShader");
            //这里有一个疑问,渲染标识符不管怎么传都不影响结果
            cmd.Blit(colorAttachment, RenderTargetHandle.CameraTarget.Identifier(), _Material);
            //执行CommandBuffer
            context.ExecuteCommandBuffer(cmd);
            //回收CommandBuffer
            CommandBufferPool.Release(cmd);
        }
    }

    CustomRenderShaderPostPass m_ScriptablePassPost;
    public Shader shader;
    /// <inheritdoc/>
    public override void Create()
    {
        m_ScriptablePassPost = new CustomRenderShaderPostPass();
        //通过创建一个渲染材质
        m_ScriptablePassPost._Material = new Material(shader);
        //定义渲染的位置
        m_ScriptablePassPost.renderPassEvent = RenderPassEvent.AfterRenderingOpaques;
    }
    // 这里你可以在渲染器中注入一个或多个渲染通道。
    //每一帧都执行
    public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
    {
        //把实例化的CustomRenderPass插入渲染管线
        renderer.EnqueuePass(m_ScriptablePassPost);
    }
}
1.实现饱和度和对比度调整

用shaderGraph实现

用shader实现

c 复制代码
Shader "Unlit/SaturationContrastShader"
{
    SubShader
    {
        Pass
        {
           ZTest Always ZWrite Off
            CGPROGRAM

            #pragma vertex vert_img  // 使用内置的vert_img顶点着色器
            #pragma fragment frag

            #include "UnityCG.cginc"

            sampler2D _CameraColorTexture; 
            //饱和度
            fixed3   Saturation_float(float3 In, float Saturation)
            {
                float luma = dot(In, float3(0.2126729, 0.7151522, 0.0721750));
                return luma.xxx + Saturation.xxx * (In - luma.xxx);
            }
            //对比度
            fixed3  Contrast_float(float3 In, float Contrast)
            {
                float midpoint = pow(0.5, 2.2);
                return (In - midpoint) * Contrast + midpoint;
            }

            fixed4 frag (v2f_img i) : SV_Target
            {
                fixed4 col = tex2D(_CameraColorTexture, i.uv);
                fixed3  Saturation =  Saturation_float(col, 5);
                fixed3  Contrast = Saturation_float(Saturation, 5);
                return fixed4(Contrast.x, Contrast.y, Contrast.z,0.1);
            }
            ENDCG
        }
    }
   FallBack Off
}

使用前

使用后

三、创建网格

关键词
1、CommandBuffer.DrawMesh

常用的接口:DrawMesh(Mesh mesh, Matrix4x4 matrix, Material material);

这个接口相对简单,传入mesh,变换矩阵,和材质

简单实现

这里接着上一个CustomRenderPassFeatureTest写,同时实现后处理和网格创建

c# 复制代码
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;

public class CustomRenderPassFeatureTest : ScriptableRendererFeature
{
    //结构说明
    class CustomRenderPass : ScriptableRenderPass
    {
        // 在执行渲染通道之前调用
        // 创建临时渲染目标纹理。
        //为Execute提前准备需要的RenderTexture或者其它变量

        public override void OnCameraSetup(CommandBuffer cmd, ref RenderingData renderingData) {}
      
        //核心方法,实现这个renderPass的逻辑
        public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData){}

        //释放在OnCameraSetup里,声明的变量如TemporaryRenderTexture
        public override void OnCameraCleanup(CommandBuffer cmd){}
    }
    //后处理效果
    class CustomRenderShaderPostPass : ScriptableRenderPass
    {  
        //定义渲染材质,通过Create方法赋值
        public Material _Material;
        public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
        {
            //创建一个CommandBuffer
            CommandBuffer cmd = CommandBufferPool.Get("ShowShader");
            //这里有一个疑问,渲染标识符不管怎么传都不影响结果
            cmd.Blit(colorAttachment, RenderTargetHandle.CameraTarget.Identifier(), _Material);
            //执行CommandBuffer
            context.ExecuteCommandBuffer(cmd);
            //回收CommandBuffer
            CommandBufferPool.Release(cmd);
        }
    }
    //绘制网格
    class CustomRenderPassCreateMesh : ScriptableRenderPass
    {
        public Material _Material;
        public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
        {
            CommandBuffer cmd = CommandBufferPool.Get("CreateMesh");
            cmd.DrawMesh(CreateMesh(), Matrix4x4.identity, _Material);
            //这里和后处理一样的操作
            context.ExecuteCommandBuffer(cmd);
            CommandBufferPool.Release(cmd);
        }
        //创建网格
        Mesh CreateMesh()
        {
            Mesh mesh = new Mesh();
            mesh.vertices = new Vector3[4] { new Vector3(1, 1, 1), new Vector3(-1, 1, 1), new Vector3(-1, 1, -1), new Vector3(1, 1, -1) };
            int[] indices = new int[8] { 0, 1, 1, 2, 2, 3, 3, 0 };
            //创建简单的线网格
            mesh.SetIndices(indices, MeshTopology.Lines, 0);
            return mesh;
        }
    }

    CustomRenderShaderPostPass m_ScriptablePassPost;
    CustomRenderPassCreateMesh m_ScriptablePassCreateMesh;
    public Shader shaderPost;
    public Shader shaderMesh;
    /// <inheritdoc/>
    public override void Create()
    {
        m_ScriptablePassPost = new CustomRenderShaderPostPass();
        //通过创建一个渲染材质
        m_ScriptablePassPost._Material = new Material(shaderPost);
        //定义渲染的位置
        m_ScriptablePassPost.renderPassEvent = RenderPassEvent.AfterRendering;

        m_ScriptablePassCreateMesh = new CustomRenderPassCreateMesh();
        m_ScriptablePassCreateMesh._Material = new Material(shaderMesh);
        m_ScriptablePassCreateMesh.renderPassEvent = RenderPassEvent.AfterRenderingOpaques;
    }

    // 这里你可以在渲染器中注入一个或多个渲染通道。
    //每一帧都执行
    public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
    {
        //把实例化的CustomRenderPass插入渲染管线
        renderer.EnqueuePass(m_ScriptablePassPost);
        renderer.EnqueuePass(m_ScriptablePassCreateMesh);
    }
}

实现效果

知识点:RenderPassEvent控制渲染的顺序,上图我们可以看到,创建的线是可以被模型遮挡和遮挡模型的。但是如果把m_ScriptablePassCreateMesh.renderPassEvent=RenderPassEvent.AfterRendering 就会发现线完全遮挡模型,即线最后渲染

四、渲染画面到图片

关键词
1、GetTemporaryRT

获取临时渲染纹理

可使用给定参数创建临时渲染纹理,并使用 nameID 将其设置为全局着色器属性。使用 Shader.PropertyToID创建整数名称。

2、Shader.PropertyToID

获取着色器属性名称的唯一标识符。

c# 复制代码
static string RtName = "_TestRT";
static int RT_ID = Shader.PropertyToID(RtName);

简单实现

同样接着上一个CustomRenderPassFeatureTest写,同时实现后处理和网格创建

c# 复制代码
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;

public class CustomRenderPassFeatureTest : ScriptableRendererFeature
{
    //结构说明
    class CustomRenderPass : ScriptableRenderPass
    {
        // 在执行渲染通道之前调用
        // 创建临时渲染目标纹理。
        //为Execute提前准备需要的RenderTexture或者其它变量

        public override void OnCameraSetup(CommandBuffer cmd, ref RenderingData renderingData) {}
      
        //核心方法,实现这个renderPass的逻辑
        public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData){}

        //释放在OnCameraSetup里,声明的变量如TemporaryRenderTexture
        public override void OnCameraCleanup(CommandBuffer cmd){}
    }
    //后处理效果
    class CustomRenderShaderPostPass : ScriptableRenderPass
    {  
        //定义渲染材质,通过Create方法赋值
        public Material _Material;
        public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
        {
            //创建一个CommandBuffer
            CommandBuffer cmd = CommandBufferPool.Get("ShowShader");
            //这里有一个疑问,渲染标识符不管怎么传都不影响结果
            cmd.Blit(colorAttachment, RenderTargetHandle.CameraTarget.Identifier(), _Material);
            //执行CommandBuffer
            context.ExecuteCommandBuffer(cmd);
            //回收CommandBuffer
            CommandBufferPool.Release(cmd);
        }
    }
    //创建网格
    class CustomRenderPassCreateMesh : ScriptableRenderPass
    {
        public Material _Material;
        public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
        {
            CommandBuffer cmd = CommandBufferPool.Get("CreateMesh");
            cmd.DrawMesh(CreateMesh(), Matrix4x4.identity, _Material);
            context.ExecuteCommandBuffer(cmd);
            CommandBufferPool.Release(cmd);
        }
        //创建网格
        Mesh CreateMesh()
        {
            Mesh mesh = new Mesh();
            mesh.vertices = new Vector3[4] { new Vector3(1, 1, 1), new Vector3(-1, 1, 1), new Vector3(-1, 1, -1), new Vector3(1, 1, -1) };
            int[] indices = new int[8] { 0, 1, 1, 2, 2, 3, 3, 0 };
            mesh.SetIndices(indices, MeshTopology.Lines, 0);
            return mesh;
        }
    }
    //渲染画面
    class CustomRenderPassRT : ScriptableRenderPass
    {
        static string RtName = "_TestRT";
        //获取着色器属性名称的唯一标识符。
        static int RT_ID = Shader.PropertyToID(RtName);
        public override void OnCameraSetup(CommandBuffer cmd, ref RenderingData renderingData)
        {
            //新建一个RenderTextureDescriptor
            RenderTextureDescriptor textureDescriptor = new RenderTextureDescriptor(1920, 1080, RenderTextureFormat.Default, 0);
            //获取临时渲染纹理
            cmd.GetTemporaryRT(RT_ID, textureDescriptor);
        }
        public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
        {
            CommandBuffer cmd = CommandBufferPool.Get("ShowRT");
            //下面三段实现效果一样
            cmd.Blit(renderingData.cameraData.renderer.cameraColorTarget, RT_ID);
           // cmd.Blit(RenderTargetHandle.CameraTarget.Identifier(), RT_ID);
           // cmd.Blit(colorAttachment, RT_ID);
            context.ExecuteCommandBuffer(cmd);
            CommandBufferPool.Release(cmd);
        }
        public override void OnCameraCleanup(CommandBuffer cmd)
        {
            cmd.ReleaseTemporaryRT(RT_ID);
        }
    }

    CustomRenderShaderPostPass m_ScriptablePassPost;
    CustomRenderPassCreateMesh m_ScriptablePassCreateMesh;
    CustomRenderPassRT m_ScriptablePassRenderTextrue;
    public Shader shaderPost;
    public Shader shaderMesh;
    /// <inheritdoc/>
    public override void Create()
    {
        #region 后处理效果
        m_ScriptablePassPost = new CustomRenderShaderPostPass();
        //通过创建一个渲染材质
        m_ScriptablePassPost._Material = new Material(shaderPost);
        //定义渲染的位置
        m_ScriptablePassPost.renderPassEvent = RenderPassEvent.AfterRendering;
        #endregion

        #region 创建网格
        m_ScriptablePassCreateMesh = new CustomRenderPassCreateMesh();
        m_ScriptablePassCreateMesh._Material = new Material(shaderMesh);
        m_ScriptablePassCreateMesh.renderPassEvent = RenderPassEvent.AfterRenderingOpaques;
        #endregion

        #region 渲染画面
        m_ScriptablePassRenderTextrue = new CustomRenderPassRT();
        m_ScriptablePassRenderTextrue.renderPassEvent = RenderPassEvent.AfterRendering;
        #endregion

    }

    // 这里你可以在渲染器中注入一个或多个渲染通道。
    //每一帧都执行
    public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
    {
        //把实例化的CustomRenderPass插入渲染管线
        renderer.EnqueuePass(m_ScriptablePassRenderTextrue);
        renderer.EnqueuePass(m_ScriptablePassPost);
        renderer.EnqueuePass(m_ScriptablePassCreateMesh);

    }
}

五、RenderFeatrue参数赋值

1、VolumeComponent

在Volume中提供Render Feature读取参数数据,通过VolumeManager单列,拿到stack,可以直接取得Volume中的数据。

以修改上文后处理的饱和度和对比度为例

1)创建一个全局的Global Volume

右击Hierarchy面板空白处选择Volume→Global Volume

2)自定义一个VolumeComponent
c# 复制代码
using UnityEngine.Rendering;

public class VolumeComponentTest : VolumeComponent
{
    public ClampedFloatParameter Saturation = new ClampedFloatParameter(1f, 0, 3);
    public ClampedFloatParameter Contrast = new ClampedFloatParameter(1f, 0, 3);
}

可以在Volume中添加VolumeComponentTest组件

3)在shader中添加控制的参数

这添加了Saturation和Contrast两个控制参数

c 复制代码
Shader "Unlit/SaturationContrastShader"
{
    Properties
    {
        _Saturation("Saturation", Range(0, 3)) = 1.0
        _Contrast("Contrast", Range(0, 3)) = 1.0
    }
    SubShader
    {
        Pass
        {
           ZTest Always ZWrite Off
            CGPROGRAM

            #pragma vertex vert_img  // 使用内置的vert_img顶点着色器
            #pragma fragment frag

            #include "UnityCG.cginc"

            float _Saturation;
            float _Contrast;
            sampler2D _CameraColorTexture; 
            //饱和度
            fixed3   Saturation_float(float3 In, float Saturation)
            {
                float luma = dot(In, float3(0.2126729, 0.7151522, 0.0721750));
                return luma.xxx + Saturation.xxx * (In - luma.xxx);
            }
            //对比度
            fixed3  Contrast_float(float3 In, float Contrast)
            {
                float midpoint = pow(0.5, 2.2);
                return (In - midpoint) * Contrast + midpoint;
            }

            fixed4 frag (v2f_img i) : SV_Target
            {
                fixed4 col = tex2D(_CameraColorTexture, i.uv);
                fixed3  Saturation =  Saturation_float(col, _Saturation);
                fixed3  Contrast = Saturation_float(Saturation, _Contrast);
                return fixed4(Contrast.x, Contrast.y, Contrast.z,0.1);
            }
            ENDCG
        }
    }
   FallBack Off
}

用shaderGraph实现的也是一样的添加两个参数

4)在RenderFeatrue中读取参数

对CustomRenderShaderPostPass修过如下

c# 复制代码
  class CustomRenderShaderPostPass : ScriptableRenderPass
    {  
        //定义渲染材质,通过Create方法赋值
        public Material _Material;
        //定义一个VolumeComponentTest
        VolumeComponentTest volumeComponentTest;
        public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
        {
            //读取VolumeManager的单列取得stack
            var stack = VolumeManager.instance.stack;
            //为volumeComponentTest赋值
            volumeComponentTest = stack.GetComponent<VolumeComponentTest>();
            if (!volumeComponentTest) return;
            //直接从volumeComponentTest中读取数据
            _Material.SetFloat("_Saturation", volumeComponentTest.Saturation.value);
            _Material.SetFloat("_Contrast", volumeComponentTest.Contrast.value);
            //创建一个CommandBuffer
            CommandBuffer cmd = CommandBufferPool.Get("ShowShader");
            //这里有一个疑问,渲染标识符不管怎么传都不影响结果
            cmd.Blit(colorAttachment, RenderTargetHandle.CameraTarget.Identifier(), _Material);
            //执行CommandBuffer
            context.ExecuteCommandBuffer(cmd);
            //回收CommandBuffer
            CommandBufferPool.Release(cmd);
        }
    }

实现效果

2、代码直接赋值

在RenderFeatrue中开放参数,用代码直接赋值

以后处理的饱和度和对比度为例

1)开放参数

在CustomRenderPassFeatureTest和CustomRenderShaderPostPass下分别定义饱和度和对比度,并赋值

c# 复制代码
public class CustomRenderPassFeatureTest : ScriptableRendererFeature
{
  
    //后处理效果
    class CustomRenderShaderPostPass : ScriptableRenderPass
    {  
        //定义渲染材质,通过Create方法赋值
        public Material _Material;
        public float _Contrast;
        public float _Saturation;
        public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
        {
            _Material.SetFloat("_Saturation", _Saturation);
            _Material.SetFloat("_Contrast", _Contrast);
            //创建一个CommandBuffer
            CommandBuffer cmd = CommandBufferPool.Get("ShowShader");
            //这里有一个疑问,渲染标识符不管怎么传都不影响结果
            cmd.Blit(colorAttachment, RenderTargetHandle.CameraTarget.Identifier(), _Material);
            //执行CommandBuffer
            context.ExecuteCommandBuffer(cmd);
            //回收CommandBuffer
            CommandBufferPool.Release(cmd);
        }
    }
    CustomRenderShaderPostPass m_ScriptablePassPost;

    public Shader shaderPost;
    public float Saturation;
    public float Contrast;
    /// <inheritdoc/>
    public override void Create()
    {
        #region 后处理效果
        m_ScriptablePassPost = new CustomRenderShaderPostPass();
        //通过创建一个渲染材质
        m_ScriptablePassPost._Material = new Material(shaderPost);
        //将饱和度和对比度赋值给CustomRenderShaderPostPass
        m_ScriptablePassPost._Contrast = Contrast;
        m_ScriptablePassPost._Saturation = Saturation;
        //定义渲染的位置
        m_ScriptablePassPost.renderPassEvent = RenderPassEvent.AfterRendering;

        #endregion
    }

    // 这里你可以在渲染器中注入一个或多个渲染通道。
    //每一帧都执行
    public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
    {
        //把实例化的CustomRenderPass插入渲染管线
        renderer.EnqueuePass(m_ScriptablePassPost);
    }
}

2)对参数进行修改

c# 复制代码
//得到RenderFeatrue    
CustomRenderPassFeatureTest custom=renderData.rendererFeatures.OfType<CustomRenderPassFeatureTest>().FirstOrDefault();
//对值进行修改
custom.Saturation =0;
custom.Contrast =0;
//设置数据
custom.SetParam();
相关推荐
逐·風5 小时前
unity关于自定义渲染、内存管理、性能调优、复杂物理模拟、并行计算以及插件开发
前端·unity·c#
_oP_i6 小时前
Unity Addressables 系统处理 WebGL 打包本地资源的一种高效方式
unity·游戏引擎·webgl
Leoysq15 小时前
【UGUI】实现点击注册按钮跳转游戏场景
游戏·unity·游戏引擎·ugui
_oP_i18 小时前
unity中 骨骼、纹理和材质关系
unity·游戏引擎·材质
Padid1 天前
Unity SRP学习笔记(二)
笔记·学习·unity·游戏引擎·图形渲染·着色器
Tp_jh1 天前
推荐一款非常好用的C/C++在线编译器
linux·c语言·c++·ide·单片机·unity·云原生
dangoxiba2 天前
[Unity Demo]从零开始制作空洞骑士Hollow Knight第十八集补充:制作空洞骑士独有的EventSystem和InputModule
游戏·unity·c#·游戏引擎·playmaker
无敌最俊朗@2 天前
unity3d————屏幕坐标,GUI坐标,世界坐标的基础注意点
开发语言·学习·unity·c#·游戏引擎
_oP_i2 天前
Unity 中使用 WebGL 构建并运行时使用的图片必须使用web服务器上的
前端·unity·webgl
司军礼2 天前
Unity自动打包——Shell交互
unity·游戏引擎·交互