-
UnityShader
2017-08-18 15:00:13Unity Shader 手册本文将逐篇翻译Unity-Manual:Shader Reference的内容。Unity手册索引: 写surface shader Surface shader示例 在surface shader中自定义光照模型 Surface shader光照示例 Surface shader中使用 DX...Unity Shader 手册
本文将逐篇翻译Unity-Manual:Shader Reference的内容。
Unity手册索引:
- 写surface shader
- 写顶点&片元 shader
- ShaderLab 语法
- Shader assets
- Advanced ShaderLab topics
文章索引:
- 写surface shader
- 写顶点&片元 shader
- ShaderLab 语法
- Shader assets
- Advanced ShaderLab topics
Shader Reference
在Unity中shader可以通过三种方式来写:
- 作为surface shaders
- 作为顶点和片元shader
- 作为fixed function shadersShader说明书会指导你来选择何种类型的shader
不管你选择的是哪种shader,实际的shader代码总会被包装为名为ShaderLab的语言,它是用来组织shader结构的,就像下面这样:
Shader "MyShader" { Properties { _MyTexture ("My Texture", 2D) = "white" { } // other properties like colors or vectors go here as well } SubShader { // here goes the 'meat' of your // - surface shader or // - vertex and program shader or // - fixed function shader } SubShader { // here goes a simpler version of the SubShader // above than can run on older graphics cards } }
我们推荐您从阅读ShaderLab的基础理念和语法的章节开始,然后继续阅读surface shaders 或者 vertex and fragment shaders的章节。由于固定管线shader只能使用ShaderLab 编写,你将在ShaderLab手册里找到更多的信息。
手册下方包含了大量不同类型shader的例子。为了获取更多特殊的shader,你可以从从Resources section获取Unity’s built-in shaders的源码。Unity的post-processing特效组件,允许您使用shader创建更多有趣的特效。
继续阅读shader手册,并看看Shader说明书。
- 写surface shader
-
unityshader教程
2017-10-13 12:14:19unityshader教程 unityshader教程 unityshader教程 unityshader教程 -
Unity Shader 基础教程
2018-05-27 19:14:23在Github上看到一篇关于Unity-Shader的教程,感觉还不错,作者写的很好,很适合Unity-Shader的基础入门,我在这里翻译一下,分享给大家,英文水平很烂,大致能明白Unity-Shader是什么,渲染管线的工作流程,以及...Unity-Shader-基础教程
在Github上看到一篇关于Unity-Shader的教程,感觉还不错,作者写的很好,很适合Unity-Shader的基础入门,我在这里翻译一下,分享给大家,英文水平很烂,大致能明白Unity-Shader是什么,渲染管线的工作流程,以及Unity Shader的一些类型和怎样编写Unity Shader。
(原文链接)第一部分:什么是Shader?
Shader是计算机图形渲染管线的一部分,它是一小段应用程序,告诉计算机在场景中怎样对物体渲染和着色。这个过程包括计算颜色和光照值,并将其给予对象,以至于对象被显示在屏幕上。和上面一样,Shader也被用来创建游戏中的一些特殊的和后期处理效果。
在现代游戏引擎中(包括Unity),Shader运行在可编程的GPU渲染管道中,在GPU中允许并行运行,并且能够很快速的进行许多着色计算。
第二部分:渲染管道
为了学习Shader,我们将简单的了解渲染管道,我们将在本教程中讨论下面这张图片:
我更加倾向把Shader看做是由一种信息类型(模型数据、颜色等等)到另外一种信息类型(像素/片元)的变换,对象数据继承与对象本身,例如模型中的点、法线、三角形、UV坐标等等。我们可以把自定义数据/传递到shader中使用,颜色、纹理、数字等等这些。
着色器流水线的第一步是顶点函数。正如你所知的,顶点就是一些点。顶点函数将处理模型中的一些点(连同其它的一些数据诸如法线)并将它们渲染流水线的下一个阶段,片元函数。
片元函数将使用这些顶点,并对它们着色。将它想象为一个画家和他们的画笔,它最终以(R,G,B,A)的格式输出像素数据。
最后,将像素添加到帧缓冲中,在帧缓冲中这些数据有可能被进一步处理,直到这些数据被绘制到屏幕上。
第三部分:Scene 配置
在开始写Shader代码之前,需要在Unity中配置一下我们的场景。在Unity中创建一个工程,并导入所有的资源。
- Bowl Model(碗模型)
- Noise Texture(噪声纹理)
- Bowl texture(碗模型纹理)
在新场景中添加一个Cube、Sphere和Bowl Model(碗模型),并保存场景,之后,你的场景将向下面这样:
接下来,在Project视图中单击右键(或者点击Create)并添加一个新的Unlit Shader(无光照着色器),将其命名为Tutorial_Shader.
如果你对其它类型的shaders好奇的话,我会在文章的末尾谈论它。
然后在刚才创建的shader上点击右键Create>Material 创建一个材质球,Unity将自动创建一个材质球并使用刚才创建的着色器的名字。
Note:一个“Material”在Unity中仅仅是着色器的一个实例,它仅保存自定义数据/属性的值。
最后,通过单击或者拖动的方式将材质赋予我们在场景中创建的所有对象上。
之后场景中的每个对象看起来是这样的,白色,并且没有阴影或者shading:
第四部分:一个Unlit Shader(无光照着色器)的大致骨架
终于到了开始写我们自己的shader的时候了,在写之前,首先打开之前创建的Tutorial_Shader文件,你将看到Unity自动生成了一些代码供我们使用。为了继续本教程,删除所有代码并使文件变空白。
Note:所有的shader在Unity中使用的是一种被称为shaderlab的语言写的。Shadrlab是HLSL/CG语法的包装器,为了使Unity能够跨平台编译Shader代码并且在Inspector面板中暴露一些属性结点。
我们将添加一些初始代码,如下:
Shader "Unlit/Tutorial_Shader"{ ... }
这行代码的作用是指定着色器代码存放在什么位置。双引号中的字符串告诉Unity在哪里查找Shader.
例如:
Shader "A/B/C/D/E_Shader"{ ... }
如果你保存你的shader代码,并切回到Unity中,你将注意到所有使用这个材质的对象都已经变成了粉红色。
当你的着色器中有错误时,Unity中将调用FallBack着色器,你将会得到一个粉红色的物体。你可以在Project中点击shader文件查看相应的错误。目前,我们得到一个粉红色的物体,因为我们的shader文件还没有完成。
接下来是属性块,如下:
Shader "Unlit/Tutorial_Shader"{ Properties{ ... } }
在属性块中,我们可以传递一些自定义数据。我们在这里声明的数据将被显示在Unity Editor面板中,在Editor中更改也会驱动脚本更改。
Shader "Unlit/Tutorial_Shader"{ Properties{ ... } SubShader{ ... } }
每一个shader有一个或者多个subshaders,如果你的应用将被部署到多种平台(移动、PC、主机),添加多个Subshader是非常有用的。例如:你可能想要写为PC/Desktop写一个高质量的Subshader,为移动端写一个低质量,但速度更快的Subshader.
pass语句块:
Shader "Unlit/Tutorial_Shader"{ Properties{ ... } Subshader{ Pass{ ... } } }
每个Subshader至少有一个pass语句块,它实际上是对象渲染的位置。一些特效要求有多个pass语句块,目前,我们仅仅专注于一个。
在pass语句块中,是一些实际渲染的代码:
Shader "Unlit/Tutorial_Shader"{ Properties{ } Sunshader{ pass{ CGPROGRAM ... ENDCG } } }
我们实际写的所有Shader代码都在CGPROGRAM和ENDCG中,对于Unity来说,shaderlab是HLSL和CG的变体。
下面,我们需要告诉Unity,顶点函数和片元函数是什么:
CGPROGRAM #pragma vertex vertexFunction #pragma fragment fragmentFunction ENDCG
这里,我们将vertex函数声明为vertexFunction,fragment函数声明为fragmentFunction.
我们也将定义这些函数:
CGPROGRAM #pragma vertex vertexFunction #pragma fragment fragmentFunction void vertexFunction(){ } void fragmentFunction(){ } ENDCG
在开始着色之前,我们需要设置一些数据结构和两个函数,这样,我们就可以使用Unity给定的数据,并把数据返回到Unity中。首先,添加UnityCG.cginc语句块,我们可以使用这个文件中包含的一些助手函数。
我们将添加一个数据结构a2v(此处和原文不一致),并修改顶点函数,将a2v作为参数传递给顶点函数。
CGPROGRAM #pragma vertex vertexFunction #pragma fragment fragmentFunction #include "UnityCG.cginc" struct a2v{ }; void vertexFunction(a2v v){ } void fragmentFunction(){ } ENDCG
当传递一个参数到vertexFunction中时,Unity将解析这个函数的结构,并基于正在绘制的对象模型传递值。我们可以传递一些自定义的数据,如下面声明的那样:
[type] [name] :[semantic]
例如,可以要求Unity获取模型对象的顶点坐标,如下:
flot4 vertex:POSITION;
我们也可以从Unity中获取顶点坐标和UV纹理坐标,如下:
struct a2v{ float4 vertex:POSITION; float2 uv:TEXCOORD0; }
最后配置顶点函数,创建一个结构体,并将其命名v2f(代表从vertex to fragment,顶点数据传递到片元),将vertex中包含的数据传递到片元函数,同时确保vertexFunction 返回 v2f的数据类型,在我们使用它时,创建并返回一个空白数据。
CGPROGRAM #pragma vertex vertexFunction #pragma fragment fragmentFunction #include "UnityCG.cginc" struct a2v{ float4 vertex:POSITION; float2 uv:TEXCOORD0; }; struct v2f{ }; v2f vertexFunction(a2v v){ v2f o; return o; } void fragmentFunction(){ } ENDCG
像之前一样,我们可以在v2f结构体中定义一些数据,我们可能想要把这些数据从顶点函数传递到片元函数。
struct v2f{ float4 position:SV_POSITION; float2 uv:TEXCOORD0; }
如果你对SV_POSITION和POSITION 感到好奇,SV代表“system value”,在v2f结构中表示最终渲染的顶点的位置。
现在基本准备好了,我们仅仅需要编辑片元函数,使它接受一个v2f结构并返回一个fixed4的值。
fixed4 fragmentFunction(v2f i){ }
输出的片元函数将是一个有(R,G,B,A)代表的颜色值
最后,我们将为片元函数添加一个SV_TARGET的输出语义,如下:
fixed4 fragmentFunction(v2f i):SV_TARGET{ }
这个过程告诉Unity我们将输出一个color去渲染,现在准备开始实际的编码了,肉和土豆使我们的vertex和fragment函数,到这个点,大致的骨架已经出来了
Shader "Unlit/Tutorial_Shader"{ Properties{ } Subshader{ Pass{ CGPROGRAM #pragma vertex vertexFunction #pragma fragment fragmentFunction #include "UnityCG.cginc" struct a2v{ float4 vertex:POSITION; float2 uv:TEXCOORD0; }; struct v2f{ float4 position:SV_POSITION; float2 uv:TEXCOORD0; }; v2f vertexFunction(a2v v){ v2f o; return o; } fixed4 fragmentFunction(v2f i):SV_TARGET{ } ENDCG } } }
第五部分:Shader 基础
首选我们要做的是获取顶点的正确位置,使用Unity中提供的UnityObjectToClipPos()函数(这个函数的作用是将世界空间的模型坐标转换到裁剪空间,函数内部封装了实现顶点坐标变换的具体细节,如矩阵变换等等),如下:
v2f vertexFunction(a2v v){ v2f o; o.position=UNnityObjectToClipPos(v.vertex); return o; }
这个函数将在局部空间中表示的顶点,变换到渲染相机的裁剪空间。注意,我们通过设置o.position的位置来传递转换的点。接下来,给片元函数一个输出。
fixed4 fragmentFunction(v2f i):SV_TARGET{ return fixed4(0,1,0,1); }
现在,等待一会儿。保存你的shader并且返回到Unity,你将看到我们的精美的绿色的物体。如下:
当然,这对你来说可能印象并不深刻,因此,让我们继续构建,而不是返回一个基本的绿色,可能我们想要编辑shader使得其能返回一个我们想要的颜色,为了做到这一点,我们需要回到开始的自定义属性。
我们可以使用如下语法添加一些属性:
name ("display name",type)=default value
如下,我们将暴露出一个颜色值,如下:
Properties{ _Color("Totally Rad Color",Color)=(1,1,1,1) }
在这里定义了一个颜色供我们使用,将其称之为_Color并且它将显示为 “Totally Rad Color!”,在Unity面板中。我们也将给予一个默认白色的值,现在保存并返回Unity,在Inspect的材质面板中,你将看到如下:
在我们使用这个color之前,我们需要把它传递到CG代码中,Unity会通过变量的名字进行自动绑定,如下:
CGPROGRAM #pragma vertex vertexFunction #pragma fragment fragmentFunction #include "UnityCG.cginc" struct a2v{ float4 vertex:POSITION; float2 uv:TEXCOORD0; }; struct v2f{ float4 position:SV_POSITION; float2 uv:TEXCOORD0; }; //从CG中获取属性 float4 _Color; v2f vertexFunction(a2v v){ v2f o; o.position=UnityObjectToClipPos(v.vertex); return o; } fixed4 fragmentFunction(v2f i):SV_TARGET{ return fixed4(0,1,0,1); } ENDCG
现在,可以在片元函数中使用_Color值了,让它返回我们期待的颜色值,而不是返回一个绿色:
fixed4 fragmentFunction(v2f i):SV_TARGET{ return _Color; }
现在,保存并返回到Unity中,如果你在Inspect中的Material中改变_Color的值,你应该能看到所有对象做出了相应的改变。
现在我们知道了如何添加属性,让我们尝试添加一张标准的纹理贴图,这里需要添加一个新的属性给我们的纹理:
Properties{ _Color("_Color",Color)=(1,1,1,1) _MainTexture("Mian Texture",2D)="white"{} }
注意它的类型是2D,默认给它一张白色的纹理,我们还需要获取这个属性在CG片段中使用它:
float4 _Color; sampler2D _MainTexTure;
然后,需要从模型传递UV纹理坐标到片元函数,我们可以通过返回顶点函数并将其传递v2f中,如下:
v2f vertexFunction(a2v v){ v2f o; o.position=UnityObjectToClipPos(v.vertex); o.ov=v.uv; return o; }
为了能在片元函数中使用纹理的颜色,我们需要对纹理进行采样。谢天谢地,CG中已经有一个tex2D()函数帮我们做了一切。
fixed4 fragmentFunction(v2f i):SV_TARGET{ return tex2D(_MainTexture,i.uv); }
tex2D获取我们想要采样的纹理以及我们想要采样的UV坐标,在这种情况下。我们提供了它的主纹理并给定模型的点,我们可以得到我们想要的颜色,最后返回的是最终的颜色。现在,保存并返回到Unity 的material insepct面板中,选择bowel纹理赋予"Main Texture"",你会发现,模型发生了改变,尤其是碗的模型看起来尤其像一碗汤。
提示:我们可以改变纹理在Unity中的采样方式,通过选择纹理文件并在Inspector面板中改变filter mode(过滤模式):
第六部分:试着改变Shader
现在,我们已经大致了解了一些基础,我们可以做一些有趣的效果并做一些简单的特效。首先,我们将使用一张噪声贴图实现“溶解” 或者“切断”效应,首先我们将添加另一个纹理属性和一个float 属性,如下:
Properties{ _Color("Color",Color)=(1,1,1,1) _MainTexture("Main Texture",2D)="white"{} _DissolveTexture("Dissolve Texture",2D)="white"{} _DissolveCutoff("Dissolve Cutoff",Range(0,1)=1 }
注意这里是如何设置_DissolveCutoff 为一个Range(0,1),它代表一个从(0,1)(包含)的float值,并且这种计数法允许我么容易的使用slider(滑动条)来设置值,接下来,让我们在CGPROGRAM中添加他们。
float4 _Color; sampler2D _MainTexture; sampler2D _DissolveTexture; float _DissolveCutoff;
现在能在片元函数中对溶解纹理采样:
fixed4 fragmentFunction(v2f i):SV_TARGET{ float4 textureColor=tex2D(_MainTexture,i.uv); float4 dissolveColor=tex2D(_DissolveCutoff,i.uv); return textureColor; }
提示:我们将为我们的主纹理使用相同的UV纹理坐标,接下里,魔术发生了:
fixed4 fragmentFunction(v2f i):SV_TARGET{ float4 textureColor=tex2D(_MainTexture,i.iv); float4 dissolveColor=tex2D(_DissolveTexture,i.uv); clip(dissolveColor.rgb-_DissolveCutoff); return textureColor; }
clip 函数检查这个给定的值是否小于0.如果小于0,我们将丢弃这个像素并且不绘制它。如果大于0,继续保持像素、正常的渲染,我们的代码的工作方式如下:
- 对主纹理的颜色进行采样
- 对纹理颜色进行裁剪采样
- 从裁剪纹理中减去裁剪值
- 如果小于0,不进行绘制
- 否则,返回主纹理采样颜色
现在,保存并返回Unity,回到材质面板,赋予“Dissolve Texture"我们的noise纹理,移动”Dissolve Cutoff" 滑动条,你应该会看到一个效果,向下面这样:
很酷吧? 我们也能做更多。在将其传递给fragment函数之前,让我们尝试更改这些顶点,在Inspector面板中暴露出一些结点属性。
Properties{ _Color("Color",Color)=(1,1,1,1) _MainTexture("Main Texture",2D)="white"{} _DissolveTexture("Dissolve Texture",2D)="white"{} _DissolveCuroff("Dissolve Cutoff",Range(0,1))=1 _ExtrudeAmount("Extrue Amount",float)=0 } ... float4 _Color; sampler2D _MainTexture; sampler2D _DissolveTexture; float _DissolveCutoff; float _ExtrudeAmount;
我们还将使用模型中的法线信息,因此,让我们添加这个字段到a2v的结构体中,以至于我们能访问它。
struct a2v{ float4 vertex:POSITION; float2 uv:TEXCOORD0; float3 normal:NORMAL; };
现在,添加一个单行到顶点函数中:
v2f vertexFunction(a2v v){ v2f o; v.vertex.xyz+=v.normal.xyz*_ExtrudeAmount; o.position=UnityObjectToClipPos(v.vertex); o.uv=v.uv; return o; }
我么在这里做的是,在将顶点转换为局部模型空间之前,我们将通过增加他们的法线方向时间来抵消它们的外加量,法线是一个向量代表顶点面向的方向,现在保存并返回Unity中,改变"Extrude Amount"的值,你应该看到下面这样的效果:
我们也能为这些属性制作动画:
v2f vertexFunction(a2v v){ v2f o; v.vertex.xyz+=v.normal.xyz*_ExtrudeAmount*sin(_Time.y); o.position=UnityObjectToClipPos(v.vertex); o.uv=v.uv; return o; }
_Time是一个代表时间的变量被包含在UnityCH.cginc中,y值代表秒,确保“Animated Materials” 在场景视图中被勾选,如下:
下面是我们最终的代码:
Shader "Unlit/Tutorial_Shader"{ Properties{ _Color("Color",Color)=(1,1,1,1) _MainTexture("Main Texture",2D)="white"{} _DissolveTexture("Dissolve Texture",2D)="white"{} _DissolveCutoff("Dissolve Cutoff",Range(0,1))=1 _ExtrudeAmount("Extrue Amount",float)=0 } Subshader{ Pass{ CGPROGRAM #pragma vertex vertexFunction #pragma fragment fragmentFunction #include "UnityCG.cginc" struct a2v{ float4 vertex:POSITION; float2 uv:TEXCOORD0; float3 normal:NORMAL; }; struct v2f{ float4 position:SV_POSITION; flaot2 uv:TEXCOORD0; }; float4 _Color; sampler2D _MainTexture; sampler2D _DissolveTexture; float _DissolveCutoff; float _ExtrudeAmount; v2f vertexFunction(a2v v){ v2f o; v.vertex.xyz+=v.normal.xyz*_ExtrudeAmount*sin(_Time.y); o.position=UnityObjectToClipPos(v.vertex); o.uv=v.uv; return o; } fixed4 fragmentFunction(v2f i):SV_TARGET{ float4 textureColor=tex2D(_MainTexture,i.uv); float4 dissolveColor=tex2D(_DissolveTexture,i.uv); clip(dissolveColor.rgb-_DissolveCutoff); return textureColor; } ENDCG } } }
第七部分:Scripting 和Shaders
接下来,我们将讨论怎样使用Unity脚本来控制Shader(即C#和Shader的交互),例如,我们将再次使用之前添加的_Color属性。首先,我们再片元函数中让其为着色器的颜色进行着色,如下:
fixed4 fragmentFunction(v2f i):SV_TARGET{ float4 textureColor=tex2D(_MainTexture,i.uv); float4 dissolveColor=tex2D(_DissolveTexture,i.uv); clip(dissolveColor.rgb-_DissolveCutoff); return textureColor*_Color; }
我们将输出颜色和_Color属性相乘,在Editor中如下:
现在,让我们开始写脚本吧,我们将为每一个对象添加一个名为RainbowColour.cs的脚本。
在脚本中,声明两个私有变量 Rendeerer和Materail:
using System.Collections; using System.Collections.Generic; using UnityEngine; public class RainbowColor:MonoBehaviour{ Renderer rend; Material material; void Start(){ } void Update(){ } }
在Satrt()函数中,设置引用值。
void Start(){ rend=GetComponent<Renderer>(); material=rend.material; }
我们将在shader中使用Material.SetColor()函数设置颜色值,这个函数的第一个参数是一个字符串,它的名字使我们想要设置的属性的名字,第二个参数是我们想要设置的颜色的值。
void Start(){ rend=GetComponent<Renderer>(); material=rend.material; material.SetColor("_Color",Color.mangenta); }
当我们运行游戏的时候,颜色变为品红。
第八部分:阴影? 表面着色器?
到目前为止,我们写了一个Unlit Shader(无光照着色器),Unity还允许你写表面着色器,表面着色器实际上就像vertex/fragment着色器,除了它们去掉了许多使着色器与光照与阴影交互的示例代码。如果你对写光照和阴影感兴趣,这是一份很棒的教程 here
在这个章节,我将展示的是,表面着色器的每个部分如何与我们的顶点/片元着色器相关联,如果你在Unity中创建一个新的“Standard Shader",你会看到一些自动生成的代码,如下:
Shader "Custom/NewSurfaceShader" { Properties{ _Color("Color",Color)=(1,1,1,1) _MainTex("Albedo(RGB)",2D)="white"{} _Glossiness("Smothness",Range(0,1))=0.5 _Metallic("Metallic",Range(0,1))=0.0 } SubShader{ Tags{"RenderType="Opaque"} LOD 200 CGPROGRAM //基于物理着色的光照模型,并且在所有光类型上启用阴影 #pragma surface surf Standard fullforwardshadows //使用3.0着色器目标,获得更好的光照效果 #pragma target 3.0 sampler2D _MainTex; struct Input{ float2 uv_MainTex; }; half _Glossiness; half _Metrllic; fixed4 _Color; //为证着色器添加实例化支持,你需要在材质上检测“启用示例" UNITY_INSTANCING_CBUFFER_START(Props) UNITY_INSTANCING_CBUFFER_END void surf(Input in,inout SurfaceOutputStandard o){ fixed4 c=tex2D(_MainTex,in.uv_MainTex)*_Color; o.Albedo=c.rgb; o.Metallic=_Metallic; o.Smoothness=_Glossiness; o.Alpha=c.a; } ENDCG } FallBack "Diffuse" }
让我们看一看每一个部分并解释一下他们都做了什么。
首先,tags标签:SubShader{ Tags{"RenderType"="Opaque"} ... }
标签帮助你告诉渲染引擎如何以及何时你的着色器被渲染。在这种情况下,我们只是指定我们的着色器是透明的,这个对于深度纹理/地图是非常有用的。
LOD 200
多细节层次或者(LOD)有助于指定再默写硬件上使用哪种着色器,LOD值越大,着色器越复杂且它的值与模型的LOD无关。
#pragma surface surf Standard fullforwardshadows
类似于我们定义顶点和片元函数,我们在这里定义了一个称之为surf的表面函数,Stadard指定Unity Shader使用标准光照模型,而fullforwardshadows指定着色器启用所有常规阴影类型。
#pragma target 3.0
这里指定编译使用的光照版本,值越大,效果越好,也越复杂,同时对系统有更高的要求。
void surf(Input i,inout SurfaceOutputStandard o){ fixed4 c=tex2D(+mAINtEX,i.uv_MianT)*_Color; o.Albedo=c.rgb; o.Metallic=_Metallic; o.Smoothness=_Glossiness; o.Alpha=c.a; }
这是着色器的核心部分,Unity定义了一个SurfaceOutputStandard 结构体来替代指定像素的颜色值。你可以设置一些诸如“Albedo"的属性,由于我们正在处理光照和阴影,不单单是直接获取颜色值,需要能够通过SurfaceOutputStandard保存的值来进行计算,下面是SurfaceOutputStandard的所有属性值的一部分:
struct SurfaceOutput{ fixed3 Albedo; fixed3 Normal; fixed3 Emission; half Specular; fixed Gloss; fixed Alpha; }
Okay,讨论一下关于verties把。
standard surface 默认情况下不暴露编辑vertices属性的函数,我们可以手动添加一个。首先,添加pragma并定义一个vertex函数#pragma surface surf Standard fullforwardshadows vertex:vert
定义vert函数:
void vert(inout appdata_full v){ v.vertex.xyz+=v.normal.xyz*_ExtrudeAmount*sin(_Time.y; ) }
提示:如果你在改变顶点坐标的时候,阴影没有随之改变,你需要确保添加了”addshadow“ paagma 片段声明,如下:
#pragma surface surf Standard fullforwardshadows vetex:vert addshadow
再表面着色器的内部是非常复杂的,但是,它最终会被编译成我们之前写的顶点和片元函数那样。我强烈的建议去读官方文档,以了解更多关于这方面的信息。
更多内容,欢迎关注公众号:
-
【Unity Shader】UnityShader基础
2017-06-07 20:30:25主要参考《Unity Shader入门精要》一书,外加自己的一些总结 前言 通过前面的学习内容可以知道,Shader并不是设么神秘的东西,它其实就是渲染流水线中的某些特定阶段,比如顶点着色器阶段、片元着色器阶段等。...主要参考《Unity Shader入门精要》一书,外加自己的一些总结
前言
通过前面的学习内容可以知道,Shader并不是设么神秘的东西,它其实就是渲染流水线中的某些特定阶段,比如顶点着色器阶段、片元着色器阶段等。什么是Unity Shader
Unity中的Shader和之前提到的渲染流水线的Shader有很大不同,我们把Unity中的Shader文件统称为Unity Shader。Unity Shader 实际上指的就是一个ShaderLab文件,硬盘上以.shader为文件后缀的一种文件。Unity Shader是Unity为开发者提供的高层次的渲染抽象层。图1 Unity Shader为控制渲染过程提供了一层抽象。如果没有使用Unity Shader(左图),开发者需要和很多文件和设置打交道,才能让画面呈现出想要的效果;而在Unity Shader的帮助下(右图),开发者只需要使用ShaderLab来编写Unity Shader文件就可以完成所有的工作Unity Shader(或者说ShaderLab文件)和传统意义上的Shader的不同:1), 在传统的Shader中,我们仅可以编写特定类型的Shader,比如顶点着色器、片元着色器等。而在Unity Shader中,我们可以在同一个文件里同时包含需要的顶点着色器和片元着色器代码。2), 在传统的Shader中,我们无法设置一些渲染设置,比如是否开启混合、深度测试等,这些是开发者在另外的代码中自行设置的。而在Unity Shader中,我们通过一行特定的指令就可以完成这些设置。3), 在传统的Shader中,我们需要编写冗长的代码来设置着色器的输入和输出,要小心的处理这些输入输出的位置对应关系等。而在Unity Shader中,我们只需要在特定语句块中声明一些属性,就可以依靠材质来方便的改变这些属性。Unity Shader提供了一种让开发者同时控制渲染流水线中多个阶段的一种方式,不仅仅是提供Shader代码。当然,Unity Shader也有一些缺点,由于Unity Shader的高封装性,我们可以编写的Shader类型和语法都被限制了,并且对于一些类型的Shader,比如曲面细分着色器、几何着色器等,Unity 的支持就相对差一些。Unity Shader概述
一个单独的Unity Shader是无法发挥任何作用的,Unity中我们需要配合使用材质(Material)和Unity Shader才能达到需要的效果。流程如下:a,创建一个材质;b,创建一个Unity Shader,并把它赋给上一步中的材质;c,把材质赋给要渲染的对象;d,在材质面板中调整Unity Shader的属性,以达到满意的效果。图2 Unity Shader 和 材质Unity提供了 4 种Unity Shader模板供我们选择——Standard Surface Shader、Unlit Shader、Image Effect Shader以及Compute Shader。其中Standard Surface Shader会产生一个包含了标准光照模型的表面着色器模板,Unlit Shader则会产生一个不包含光照但包含雾效的基本的顶点/片元着色器,Image Effect Shader 则会为我们实现各种屏幕后处理效果提供一个基本模板,Compute Shader会产生一种特殊的Shader文件,这类Shader旨在利用GPU的并行性来进行一些与常规渲染流水线无关的计算。什么是ShaderLab
在Unity中,所有的Unity Shader 都是使用ShaderLab来编写的,ShaderLab是Unity提供的编写Unity Shader的一种说明性语言。它使用了一些嵌套在花括号内部的语义来描述一个Unity Shader 文件的结构。一个Unity Shader 的基础结构如下所示:Shader "ShaderName"{ Properties{ //属性 } SubShader{ //显卡A使用的子着色器 } SubShader{ //显卡B使用的子着色器 } FallBack "VertexLit" }
Unity在背后会根据使用的平台来把这些结构编译成真正的代码和Shader文件,而开发者只需要和Unity Shader打交道即可。在Unity 中,属性的名字通常由一个下划线开始,我们需要给每个属性指定它的类型,并指定默认值。下面的代码给出了一个展示所有属性类型的例子:
SubShader:Shader "Custom/ShaderLabProperties" { Properties { // Numbers and Sliders _Int ("Int",Int) = 2 _Float("Float",Float) = 1.5 _Range("Range",Range(0.0,5.0)) = 3.0 // Colors and Vectors _Color ("Color",Color) = (1,1,1,1) _Vector ("Vector",Vector) = (2,5,3,1) //Textures _2D("2D",2D) = ""{} _Cube("Cube",Cube) = "white"{} _3D("3D",3D) = "black"{} } FallBack "Diffuse" }
每一个Unity Shader 文件可以包含多个SubShader语义块,但至少要有一个。当Unity 需要加载这个Unity Shader 时,Unity会扫描所有的SubShader语义块,然后选择第一个能够在目标平台上运行的SubShader。如果都不支持的话,Unity 就会使用FallBack 语义指定的 unity Shader。SubShader语义块包含的定义通常如下:SubShader{ //可选的 [Tags] //可选的 [RenderSetup] Pass{ } //oteher Passes }
SubShader中定义了一系列Pass 以及可选的状态([RenderSetup]) 和标签([Tags])设置。每个Pass定义了一次完整的渲染流程,但如果Pass数目过多,会造成渲染性能的下降。SubShader中的标签设置是特定的,和Pass中使用的标签不一样。但对于状态来说,使用的语法是相同的,如果在SubShader进行了状态设置,那么将会用于所有的Pass。ShaderLab提供了一系列渲染状态的设置指令,这些指令可以设置显卡的各种状态,如下表:SubShader 的标签Tags:SubShader的标签是一个键值对(Key/Value Pair),它的键和值都是字符串类型。这些键值对是SubShader 和渲染引擎之间的沟通桥梁,标签的结构如下:Tags{ "TagName1" = "Value1" "TagName2" = "Value2"}上面表里的标签仅可以在SubShader中声明,不可以在Pass块中声明,Pass的标签类型如下表:FallBack:作用:告诉Unity,如果所有的SubShader在这显卡上不能运行,就使用这个最低级的Shader。事实上,FallBack会影响阴影的投射。在渲染阴影纹理时,Unity会在每个Unity Shader中寻找一个阴影投射的Pass。通常情况下,我们不需要自己专门实现一个Pass,这是因为FallBack 使用的内置Shader中包含了这样一个通用的Pass,所以,为每个Unity Shader正确设置FallBack是非常重要的。Unity Shader的形式
表面着色器(SurFace Shader):表面着色器是Unity自己创造的一种着色器代码类型。可以理解成,表面着色器是Unity对顶点/片元着色器的更高一层的抽象,它存在价值在于,Unity为我们处理了很多光照细节。一个非常简单的表面着色器示例代码如下:
表面着色器定义在SubShader语义块(非Pass语义块)中的CGPROGRAM和ENDCG之间,是使用CG/HLSL编写的。Shader "Custom/Simple Surface Shader" { SubShader{ Tags{ "RenderType" = "Opaque"} CGPROGRAM #pragma surface surf Lambert struct Input { float4 color : COLOR; }; void surf(Input IN, inout SurfaceOutput o) { o.Albedo = 1; } ENDCG } FallBack "Diffuse" }
顶点/片元着色器(Vertex/Fragment Shader):一个简单的顶点/片元着色器示例代码如下:
顶点/片元着色器的代码也需要定义在CGPROGRAM和ENDCG之间,也是用CG/HLSL编写的,但不同的是,顶点/片元着色器是写在Pass语义块内,而非SubShader内的。Shader "Custom/Simple VertexFragment Shader" { SubShader{ Pass{ CGPROGRAM #pragma vertex vert #pragma fragment frag float4 vert(float4 v : POSITION) : SV_POSITION{ return mul(UNITY_MATRIX_MVP,v); } float4 frag() : SV_Target{ return fixed4(1.0,0.0,0.0,1.0); } ENDCG } } FallBack "Diffuse" }
选择哪种Unity Shader 形式?a,除非有非常明确的需求必须使用固定函数着色器,比如需要在非常旧的设备上运行你的游戏,否则就使用可编程管线的着色器,即表面着色器或顶点/片元着色器。b,如果你想和各种光源打交道,可以选择表面着色器,但需要注意它在移动平台的性能表现。c,如果你需要使用的光照数目非常少,比如只有一个平行光,那么使用顶点/片元着色器是一个更好的选择。c,如果你有非常多自定义的渲染效果,那么请选择顶点/片元着色器。Unity Shader 和 CG/HLSL之间的关系
Unity Shader 是ShaderLab 语言编写的,但对于表面着色器和顶点/片元着色器,我们可以在ShaderLab内部嵌套CG/HLSL语言来编写这些着色器代码。这些CG/HLSL 代码是嵌套在CGPROGRAM 和 ENDCG 之间的,由于CG 和 HLSL 从写法上几乎是同一种语言,因此在Unity 里CG 和 HLSL 是等价的。表面着色器在本质上就是顶点/片元着色器,他们看起来很不想是因为表面着色器是Unity 在顶点/片元着色器上层为开发者提供的一层抽象封装,但在背后,Unity 还是会把它转化成一个包含多Pass的顶点/片元着色器。 所以,从本质上来讲,Unity Shader 只有两种形式: 顶点/片元着色器和固定函数着色器(Unity5.2以后的版本,固定函数着色器也会在背后被转化成顶点/片元着色器,因此,Unity本质上只存在顶点/片元着色器)。当然,Unity Shader 也可以用GLSL来写,但是这意味着你可以发布的目标平台就只有 Mac OS X、OpenGL ES 2.0或者Linux,而对于PC 、Xbox 360这样的仅支持DirectX的平台来说,就不行了。 -
UnityShader4:UnityShader的形式
2020-11-20 15:56:16前置:UnityShader2:Shader与材质 一、Standard Surface Shader 代码解析 在前置中已经了解了什么是表面着色器,下面这是Standard Surface Shader,也是创建 Shader 时的默认代码之一 Shader "Custom/...一、Standard Surface Shader 代码解析
在前置中已经了解了什么是表面着色器,下面这是 Standard Surface Shader,也是创建 Shader 时的默认代码之一
Shader "Custom/NewSurfaceShader" { Properties { _Color("Color", Color) = (1,1,1,1) _MainTex("Albedo (RGB)", 2D) = "white" {} _Glossiness("Smoothness", Range(0,1)) = 0.5 _Metallic("Metallic", Range(0,1)) = 0.0 } SubShader { Tags { "RenderType" = "Opaque" } LOD 200 CGPROGRAM #pragma surface surf Standard fullforwardshadows #pragma target 3.0 sampler2D _MainTex; struct Input { float2 uv_MainTex; }; half _Glossiness; half _Metallic; fixed4 _Color; UNITY_INSTANCING_BUFFER_START(Props) UNITY_INSTANCING_BUFFER_END(Props) void surf (Input IN, inout SurfaceOutputStandard o) { fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color; o.Albedo = c.rgb; o.Metallic = _Metallic; o.Smoothness = _Glossiness; o.Alpha = c.a; } ENDCG } FallBack "Diffuse" }
直接一行一行注释(对于紫色的内容,可以
暂时不用去详细了解):- Tags { "RenderType" = "Opaque" }:这个在 UnityShader3:ShaderLab 这一章中有过讲解,声明为不透明着色器
- LOD 200:设置当前着色器的 LOD(Shader Level of Detail) 值,只有其小于 Shader.globalMaximumLOD 全局最大细节级别时这个 SubShader 才会被执行,注意这个和模型 LOD 不是一个东西
- CGPROGRAM …… CGEND:在这两句声明之间的代码是着色器语言的主要部分,并且是用 CG(C for Graphics) 编写的,CG 和 GLSL 一样,是一种着色器语言
- #pargma 关键词 函数名 光照模型 [其它选项]:编译指令声明,代码中的含义为:表明当前为表面着色器、函数名为 surf、使用 Standard 基于物理系统的光照模式、有一个完整的向前的阴影
- #pragma target 3.0:设置着色器模型等级,越高支持的功能越多,可以参考文档
- sampler2D:对应二维纹理,在 Properties 中为 2D 属性
- half:描述的是一个16位浮点数,同理 float 为32位浮点数
- fixed4:对应4维颜色属性,同理 half4 就是4个 float 属性组成的4维向量
- struct Input { float2 uv_MainTex; }:输入结构体用于模述 UV 的坐标,里面的 float2 变量必须以 uv_开头,后面的 _MainTex 自动对应 Properties 中的 _MainTex,理论上这个结构体里还可以有其它成员,不过目前只需要关心纹理坐标
- UNITY_INSTANCING_BUFFER_START:用于批量渲染,减少 DrawCall 次数,超纲了,可以先去了解下 OpenGL 的实例化操作:《OpenGL基础42:实例化》
void surf (Input IN, inout SurfaceOutputStandard o) {}:第一个参数 Input IN 就是前面定义的 Input 结构体,第二个参数用了 inout 声明,这表示这个参数既是输入的也是输出的,这个 surf 函数虽然没有返回值,但是它的第二个参数是有输出功能
从上述程序中可以看出:表面着色器被定义在 SubShader 语义块中的 CGPROGRAM 和 ENDCG 之间,并且没有 Pass 语义块,这是因为表面着色器不需要开发者关心使用了多少个 Pass、每个 Pass 如何渲染等问题,Unity 会在背后为我们做好这些事情,我们要做的只是告诉它使用哪些纹理去填充颜色,如何填充法线,使用哪种光照模型等
上面的代码对应的预览效果(无纹理):
二、固定函数着色器
固定渲染管线已经过时了,没有必要去学习/关心,但也可以在这一章顺便提一下
一个非常简单的固定函数着色器例子如下:
Shader "Tutorial/Basic" { Properties { _Color("Main Color", Color) = (1,0.5,0.5,1) } SubShader { Pass { Material { Diffuse [_Color] } Lighting On } } }
关于固定函数着色器可以了解的是:
- 固定函数着色器需要完全按照 ShaderLab 的语法,而非使用 CG/HLSL
- 在 Unity 5.2 以后的版本中,固定函数着色器也会被 Unity 编译成对应的顶点/片元着色器,因此真正意义上的固定函数着色器已经不存在了
- 部分旧设备只支持固定管线,这样就只能用固定函数着色器了,当然一般抛弃这些平台是最好的选择
三、扩展
UnityShader形式有三种,前面主要提到了表面着色器和固定函数着色器,然而事实上,最常用的还是顶点/片元着色器,可以自己实现一些五花八门的渲染效果,只不过关于顶点片元着色器,需要了解的东西不少,这篇就暂时不讲了
传统的 Shader 和 UnityShader 的区别有:
- UnityShader 可以在一个文件里同时包含需要的顶点着色器和片元着色器代码,而传统着色器需要一一对应
- UnityShader 处理输入和输出非常的简单/方便,只需要在特定语句块中声明一些属性就可以依靠材质来方便地改变它们,而对于模型自带的数据:如顶点位置、纹理坐标、法线等, UnityShader 也提供了直接访问的方法
- UnityShader 只需要通过一行特定的指令即可以完成一些渲染设置,例如是否开启混合、深度测试等,而对于传统 Shader 开发者需要自行在外环境设置
- UnityShader 对曲面细分着色器(Tessellation Shader)、几何着色器(Geometry Shader)不能够很好的支持
关于 CG/HLSL 和 GLSL:
对于上面的表面着色器,是通过在 ShaderLab 内部嵌套 CG/HLSL 语言来编写这些着色器代码的,由于 CG 和 DX9 风格的 HLSL 从写法上来说几乎是同一种语言,因此在 Unity 里 CG 和 HLSL 等价
当然也可以用 GLSL 来写,不过这样对于PC、Xbox 360这些的仅支持DirectX的平台来说,就不能正常的运行了
本文[系列]参考于:UnityShader入门摘要
其它参考资料:https://docs.unity3d.com/Manual/SL-Reference.html
https://docs.unity3d.com/Manual/ShaderTut1.html
-
Unity Shader小白入门 (3) Unity Shader的形式
2019-03-17 22:35:25一、 UnityShader 整体概况 Unity Shader文件的结构 ShaderLab的语法 UnityShader的主要任务——指定着色器所需的代码 写在SubShader语义块中(表面着色器) 写在Pass语义块中(顶点/片元着色器,固定函数... -
《unity shader 入门精要》读书笔记2 - Unity Shader基础
2019-11-17 14:49:33什么是ShaderLab? " Shaderlab is a ...Unity Shader是Uniy为开发者提供的高层级的渲染抽象层,Shaderlab是Uniy提供的编写 Unity Shader的一种说明性语言。 左图是未使用unity shader,右图为unity shader! ... -
《Unity Shader入门精要》 第三章 Unity Shader 基础 笔记
2019-04-01 15:31:58Unity Shader 基础 shader和材质 基本流程 创建材质 创建unity shader,并赋给创建的材质 将材质赋给要渲染的对象 在材质面板调整Unity Shader属性。 Unity Shader 模板: standard surface shader:包含了标准... -
Unity Shader——《Unity Shader入门摘要》学习目录
2018-10-21 15:43:021.Unity Shader初学——《Unity Shader入门摘要》 https://blog.csdn.net/weixin_42513339/article/details/83239843 2.Unity Shader——GPU流水线 ...3.Unit... -
Unity Shader之路(一)什么是Unity Shader?
2020-07-29 17:51:28Unity Shader前言什么是UnityShaderUnity Shader和材质的关系 前言 众所周知,要想当游戏开发工程师,Unity中的Shader编写不得不摸,这一系列文章我将描写我的Shader学习之路,致敬冯乐乐的书Unity Shader入门精要,... -
Unity Shader入门精要
2018-03-31 12:17:51Unity Shader入门精要 Unity Shader入门精要 Unity Shader入门精要Unity Shader入门精要 -
Unity Shader编程(2)Unity Shader初步
2015-10-03 15:21:54Unity Shader编程(2)Unity Shader初步 Unity通过ShaderLab来组织Shader。 下面我们通过分析上一节中的Shader代码来学习Unity Shader的组织形式。 Shader "husheng/Shader" { Properties { _Color ("Color", ... -
Unity Shader 入门精要
2018-08-15 11:16:54Unity Shader 入门精要,Unity Shader 入门精要,Unity Shader 入门精要 -
Unity Shader入门精要学习笔记 - 第3章 Unity Shader 基础
2017-12-26 17:05:21Unity Shader入门精要学习笔记 - 第3章 Unity Shader 基础本系列为UnityShader入门精要读书笔记总结, 原作者博客链接:http://blog.csdn.net/candycat1992/article/ 书籍链接:... -
Unity Shader编程
2018-03-27 17:25:32Unity Shader编程 -
【Unity Shader入门精要】— Unity Shader基础
2017-10-18 14:05:31引言Unity Shader是Unity为开发者提供的高层级的渲染抽象层,它让开发者更加轻松地管理着色器代码以及渲染设置(如开启/关闭混合、深度测试、设置渲染状态等)。Unity Shader的概述Unity为我们准备的常用的三种... -
Unity Shader入门精要笔记(二):Unity Shader基础
2017-06-20 00:28:27本系列文章由Aimar_Johnny编写,欢迎转载,转载请标明出处,谢谢。...1)在Unity中我们需要配合使用材质(material)和Unity Shader才能达到需要的效果。材质是载体,Unity Shader是文本文件,我们在Unit -
Unity Shader 学习笔记(4)Unity Shader内置变量、函数,Shader Model
2017-11-12 16:54:58Unity Shader 学习笔记(4)Unity Shader内置变量、函数内置文件下载Unity 下载存档内置函数 官方API:Built-in shader helper functions 民间翻译总结:shader内置函数 [zhang273162308] Unity&Shader基础篇-... -
unity shader
2013-11-30 20:08:03Unity3D 自带的 60 多个 Shader。 这些 Shader 被分为五个大类:Normal,Transparent,Transparent Cutout,Self-llluminated,Reflective。由于数量比较多,将分几个篇幅一一介 绍。 (一) Normal Shader Family ... -
Unity Shader 学习笔记(3)Unity Shader模板、结构、形式
2017-11-10 15:05:32Unity Shader 学习笔记(3)Unity Shader模板、结构、形式 参考书籍:《Unity Shader 入门精要》 Unity Sahder的模板 模板名 功能 Standard Surface Shader 包含标准光照模型的表面着色器模板。 Unlit Shader ... -
Unity Shader入门精要学习笔记 - 第5章 开始 Unity Shader 学习之旅
2017-12-27 16:14:51Unity Shader入门精要学习笔记 - 第5章 开始 Unity Shader 学习之旅本系列为UnityShader入门精要读书笔记总结, 原作者博客链接:http://blog.csdn.net/candycat1992/article/ 书籍链接:... -
《Unity Shader 入门精要》学习笔记03---Unity Shader基础
2019-10-04 14:29:30声明:只是自己看书的...在Unity中,要使用Unity Shader需要先建一个材质,这个材质时Unity Shader的载体。 Unity 中的基本Shader模板有4种: 1.Standard Surface Shader(包含了标准光照模型的表面着色器模板) 2.... -
Unity Shader基础(3)--unity shader 常用语义
2016-10-26 22:17:37摘自冯乐乐的《unity shader 入门精要》 由于之前看《Unity shader 入门精要》看到太过粗略了,只关注书中的shader代码,导致好多代码都是不是很懂,所以就往回看, 找找对于shader的一些基础知识 1、 ... -
Unity Shader学习笔记 — UnityShader入门精要中出现的错误(1)
2019-11-27 14:56:45Unity Shader入门概要的第五章的5.2中有这样的一段代码: Shader "Unity Shaders Book/Chapter 5/Simple Shader"{ SubShader { Pass{ CGPROGRAM #pragma vertex vert #pragma fragment frag ... -
Unity Shader学习:ShaderToy与Unity
2019-01-08 16:16:11Unity Shader学习:ShaderToy与Unity ShaderToy是个非常有意思的网站,里面都是些图形学大神反人类的骚操作,这里学乐乐大佬的文章将ShaderToy的代码和Unity的shader代码做个移植实现。 本案例ShaderToy地址(打开要... -
【unity shader】《unity shader入门精要》 光照模型
2016-06-13 20:58:19unity shader中的漫反射+高光反射光照模型 光照模型是shader的核心,它描述了光线同物体的交互方式。 对于非透明物体,光照模型一般包含两部分:漫反射和高光反射。 一个常见的包含漫反射与高光反射的shader程序... -
Unity ShaderGraph节点详解
2019-01-10 11:18:48Unity ShaderGraph节点详解,最新的unity材质shader节点编辑器,节点示意非常 清楚,方便大伙入门 -
UnityShader与Shader的区别
2018-10-24 17:21:26一、UnityShader与Shader 在Unity里,Unity Shader实际上指的就是一个ShaderLab文件——硬盘上以.shader作为文件后缀的一种文件。 Unity Shader相比于Shader,有优点也有缺点。 优点有: 在传统的... -
【Unity Shader】浅析Unity shader中RenderType的作用及_CameraDepthNormalsTexture
2016-01-13 17:21:01初学Unity ShaderLab的时候,一定有接触过Unity Shader中的Tags标签块,比如: "LightMode"="Vertex" "Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" "PreviewType"="Plane" ...
-
MySQL 多平台多模式(安装、配置和连接 详解)
-
项目-源码
-
Amoeba 实现 MySQL 高可用、负载均衡和读写分离
-
伊瓜奴-源码
-
音阶理论:我对音阶做了很多思考-源码
-
云开发后台+微信扫码点餐小程序+cms网页管理后台 含后厨端和用户端
-
分子束外延生长InPBi薄膜的结构和光学表征
-
产品规划委员会管理制度V1.3.docx
-
layui 表单标签的校验,解决layui非必填项却依然校验的bug
-
光催化在杂化卤化钙钛矿表面上的氧嵌入
-
MySQL 存储过程(创建海量数据实验环境)
-
朱老师鸿蒙系列课程第1期-2鸿蒙系统Harmonyos源码架构分析
-
layUi框架学习入门篇(三)表单用法及其使用案例
-
2021年初Java核心面试总结:Java+Redis+数据库+解决方案+分布式
-
美食街-源码
-
左右箭头(背景透明)资源包.rar
-
MCA-2021-源码
-
MySQL 设计基础(数据库概论、初探)
-
redis探索之熟悉string类型操作
-
字计数器-源码