【UnityShader入门精要学习笔记】第十七章 表面着色器
本系列为作者学习UnityShader入门精要而作的笔记,内容将包括:
- 书本中句子照抄 + 个人批注
- 项目源码
- 一堆新手会犯的错误
- 潜在的太监断更,有始无终
我的GitHub仓库
总之适用于同样开始学习Shader的同学们进行有取舍的参考。
文章目录
- 表面着色器
- 表面着色器的一个例子
- 编译指令
- 表面函数
- 光照模型
- 其他可选参数
- 两个结构体
- 数据来源:Input 结构体
- 表面属性:SurfaceOutput结构体
- Unity背后做了什么
- 表面着色器实例分析
浅看了第十八章,感觉没必要写。所以这一章表面着色器就是我们的最终章了,接下来重心将会落实到一些深入引擎和优化技术上。
表面着色器
顶点片元着色器本质上是一种对硬件友好的方式,但是对人类不友好。虽然计算机世界中这样的例子已经比比皆是了。出于拒绝反人类的目的,一种新的着色器表面着色器(Surface Shader) 被加入到Unity中。
表面着色shader包含了3个层次:表面着色器,光照模型和光照着色器
其中表面着色器定义了模型表面的反射率、法线和高光等,光照模型则选择使用的光照模型类型,是半兰伯特?还是BlinnPhong或者其他光照模型?光照着色器则由系统进行实现。
表面着色器大大减少了shader开发的工作量,大多数时候我们只需要和表面着色器打交道,定义一些属性,选择要使用的光照模型即可。
表面着色器实际上就是对顶点片元着色器上的一层抽象,使用表面着色器可以用一种更容易理解的方式编写shader,不需要考虑前向渲染路径还是延迟渲染路径,场景中的光源等等等等要素。
表面着色器的一个例子
要实现一个前向光照渲染的材质,我们要定义光照模型,定义前向渲染Pass,定义阴影pass。总而言之,如果用顶点片元着色器实现会很难,很复杂。
但是现在我们可以使用表面着色器来直接实现:
Shader "Custom/BumpedDiffuse_Copy"
{Properties{_Color("Main Color",Color) = (1,1,1,1)_MainTex("Base",2D) = "white"{}_BumpMap("Normalmap",2D) = "bump"{}}SubShader{Tags{"RenderType" = "Opaque"}LOD 300CGPROGRAM#pragma surface surf Lambert#pragma target 3.0sampler2D _MainTex;sampler2D _BumpMap;fixed4 _Color;struct Input{float2 uv_MainTex;float2 uv_BumpMap;};void surf(Input IN,inout SurfaceOutput o){fixed4 tex = tex2D(_MainTex,IN.uv_MainTex);o.Albedo = tex.rgb * _Color.rgb;o.Alpha = tex.a * _Color.a;o.Normal = UnpackNormal(tex2D(_BumpMap,IN.uv_BumpMap));}ENDCG}Fallback "Diffuse"
}
如此简单就能完成一个光照模型,与顶点片元着色器需要包含到一个特定的Pass块中不同, 表面着色器可以直接且必须写在SubShader块中,Unity会自动生成多个Pass,并在CGPROGRAM块中定义表面着色器的具体代码。
编译指令
在表面着色器中,我们通过编译指令来和Unity进行沟通,编译指令最重要的作用是指示该表面着色器使用的表面函数和光照函数,并设置一些可选参数。表面着色器的CG块中的第一句代码往往就是它的编译指令,编译指令的一般格式如下:
#pragma surface surfaceFunction lightModel [optionalparams]
指定表面函数和光照模型,以及其他的一些可选参数来控制表面着色器的一些行为。
表面函数
表面着色器抽象出了表面这一概念,并包含了表面属性,一个对象的表面属性定义了它的反射率、光滑度、透明度等值。而编译指令中的surfaceFunction就用于定义这些表面属性。surfaceFunction通常就是名为surf的函数(函数名可以任意),它的函数格式是固定的:
void surf(Input IN, inout SurfaceOutput o)
void surf(Input IN, inout SurfaceOutputStandard o)
void surf(Input IN, inout SurfaceOutputStandardSpecular o)
其中输入结构体Input是我们自定义的,用于设置各种表面属性。
输出结构体通常是SurfaceOutput
,SurfaceOutputStandard
,SurfaceOutputStandardSpecular
,这些类型,它们是unity内置的结构体,需要配合不同的光照模型使用。
光照模型
官方自定义光照文档
除了表面函数,我们还需要指定光照函数,光照函数会使用表面函数中设置的各种表面属性来应用某些光照模型。
unity中内置了基于物理的光照模型Standard和StandardSpecular,以及简单的非基于物理的Lambert和BlinnPhong
当然我们也可以定义自己的光照函数,例如用下列函数来定义前线渲染中的光照函数:
// 根据官方文档的代码,Lighting是统一的前缀,后面字符部分才是光照模型的名称
// 一些常用的变量直接按规定属性名定义为函数入参并使用即可
// 用于不依赖视角的光照模型,例如漫反射
half4 Lighting<Name> (SurfaceOutput s,half3 lightDir, half atten);
// 用于依赖视角的光照模型,例如高光反射
half4 Lighting<Name> (SurfaceOutput s,half3 lightDir, half3 virwDir, half atten);
其他可选参数
除了光照模型和表面着色器两个必需参数之外,我们还可以设置一些可选参数
这些参数都在官方文档中记载了
简单的使用可选参数就可以定义一些功能
注意表面着色器只能在内置渲染管线Build-In Pipeline中使用,而URP和HDRP是不能使用的,在URP和HDRP中想要简单的实现Shader需要使用Shader Graph
两个结构体
表面着色器最多支持自定义4种关键的函数:
- 表面函数(用于设置各种表面性质,如反射率,法线等)
- 光照函数(定义表面使用的光照模型)
- 顶点修改函数(修改或传递顶点属性)
- 最后的颜色修改函数(对最后的颜色进行修改)
那么,这些函数之间的信息传递是如何实现的呢?
一个表面着色器需要使用两个结构体:表面函数的输入结构体Input,以及存储了表面属性的结构体SurfaceOutput
数据来源:Input 结构体
Input结构体包含了许多表面属性的数据来源,因此,它会作为表面函数的输入结构体。Input支持很多内置的变量名,通过这些变量名,我们告诉Unity需要使用的数据信息,例如,在Input结构体种包含了主纹理和法线纹理的采样坐标uv_MainTex和uv_BumpMap。这些采样坐标必须以uv为前缀
因此纹理的采样直接用uv_sample2Dname
这种格式定义即可,而其他变量需要根据下表严格定义!
只需要定义变量即可使用了
除了这些变量之外,如果我们想要自定义变量也可以,就像顶点片元着色器种实现的一样,自定义变量并在顶点修改函数中进行处理,将其传递到surface函数中去。
表面属性:SurfaceOutput结构体
SurfaceOutput就是专门用于存储表面属性的。SurfaceOutput
,SurfaceOutputStandard
,SurfaceOutputStandardSpecular
等,它们作为表面着色器的输出,也作为光照函数的输入。这些结构体的变量都是提前声明好的,直接使用即可。
剩下的就是光照模型,可以用内置的光照模型,我们也可以自定义光照函数
Unity背后做了什么
使用表面着色器,我们只需要编译指令、自定义函数和两个结构体就可以生成一个表面着色器。而实际上Unity在背后为表面着色器生成了一系列的顶点/片元着色器
Unity在背后会根据表面着色器生成一个包含了很多Pass的顶点/片元着色器,例如我们设置了不同的渲染路径则会生成对应渲染路径的Pass,我们设置了不同LightMode则会生成不同的对应光照的Pass,若使用了addshadow则会生成ShadowCaster的阴影Pass。
我们只需在Unity 的表面着色器的面板上点击Show generated code即可生成对应的顶点/片元着色器代码。
根据上图可以看到,其实表面着色器的过程很简单。我们之前定义的部分都是属于片元着色器中的自定义部分。
Unity对Pass的自动生成过程如下:
(1)直接将表面着色器中CGPROGRAM块部分的代码赋值并解析,这部分代码包括了我们对预编译指令,定义的变量,以及表面函数光照函数等。这些函数和变量将在处理后进行调用
(2)unity分析代码,并生成顶点着色器的输出v2f_surf
结构体,用于顶点着色器和片元着色器之间的变量传递。 v2f_surf
结构体中的变量是根据我们定义的相应变量生成的,注意变量名称要一模一样。若某些变量在编译时发现未使用,也不会被生成带v2f_surf
结构体中。
(3)接着生成顶点着色器vert_surf
- 若我们自定义了顶点修改函数,则unity会首先调用顶点修改函数来修改顶点数据,或填充自定义的Input结构体中的变量,然后Unity会分析顶点修改函数中修改的数据,并通过Input结构体将修改结果存储到v2f_surf相应的变量中。
- 将顶点数据计算成一些其他的通用变量,例如顶点坐标,纹理坐标,法线方向,逐顶点光照,光照纹理的采样坐标等。可以通过编译器控制某些变量是否需要计算
- 最后将
v2f_surf
传递到下一片元着色器中
(4)生成片元着色器frag_surf
- 使用顶点着色器传递的
v2f_surf
结构体变量来填充Input
结构体,例如纹理坐标,视角方向等 - 调用自定义的表面函数填充
SurfaceOutput
结构体 - 调用光照函数得到初始的颜色值,如果使用的是内置的Lambert和BlinnPhong光照模型,还会计算动态全局光照
- 进行其他的颜色叠加,例如若没有使用光照烘焙,还会添加逐顶点光照
- 最后,如果自定义了最后的颜色修改函数,unity会调用它进行最后的颜色修改
表面着色器实例分析
文中的表面着色器实例分析针对的是表面着色器生成的顶点/片元着色器代码,具体可以详见书本
我们此处简单分析一下表面着色器的代码:
Shader "Unity Shaders Book/Chapter 17/Normal Extrusion" {Properties {_ColorTint ("Color Tint", Color) = (1,1,1,1)_MainTex ("Base (RGB)", 2D) = "white" {}_BumpMap ("Normalmap", 2D) = "bump" {}_Amount ("Extrusion Amount", Range(-0.5, 0.5)) = 0.1}SubShader {Tags { "RenderType"="Opaque" }LOD 300CGPROGRAM// surf - which surface function.// 自定义的光照模型// CustomLambert - which lighting model to use.// 自定义的顶点修改函数// vertex:myvert - use custom vertex modification function.// 自定义的颜色修改函数// finalcolor:mycolor - use custom final color modification function.// 编译选项——生成阴影// addshadow - generate a shadow caster pass. Because we modify the vertex position, the shder needs special shadows handling.// 编译选项——不为deferred/legacy deferred渲染路径生成pass// exclude_path:deferred/exclude_path:prepas - do not generate passes for deferred/legacy deferred rendering path.// 不生成用于全局动态光照的“meta” pass// nometa - do not generate a “meta” pass (that’s used by lightmapping & dynamic global illumination to extract surface information).#pragma surface surf CustomLambert vertex:myvert finalcolor:mycolor addshadow exclude_path:deferred exclude_path:prepass nometa#pragma target 3.0fixed4 _ColorTint;sampler2D _MainTex;sampler2D _BumpMap;half _Amount;struct Input {float2 uv_MainTex;float2 uv_BumpMap;};// 自定义顶点修改函数void myvert (inout appdata_full v) {v.vertex.xyz += v.normal * _Amount;}void surf (Input IN, inout SurfaceOutput o) {fixed4 tex = tex2D(_MainTex, IN.uv_MainTex);o.Albedo = tex.rgb;o.Alpha = tex.a;o.Normal = UnpackNormal(tex2D(_BumpMap, IN.uv_BumpMap));}// 自定义的光照模型half4 LightingCustomLambert (SurfaceOutput s, half3 lightDir, half atten) {half NdotL = dot(s.Normal, lightDir);half4 c;c.rgb = s.Albedo * _LightColor0.rgb * (NdotL * atten);c.a = s.Alpha;return c;}// 自定义的颜色修改函数void mycolor (Input IN, SurfaceOutput o, inout fixed4 color) {color *= _ColorTint;}ENDCG}FallBack "Legacy Shaders/Diffuse"
}
相关文章:

【UnityShader入门精要学习笔记】第十七章 表面着色器
本系列为作者学习UnityShader入门精要而作的笔记,内容将包括: 书本中句子照抄 个人批注项目源码一堆新手会犯的错误潜在的太监断更,有始无终 我的GitHub仓库 总之适用于同样开始学习Shader的同学们进行有取舍的参考。 文章目录 表面着色器…...
Python社会经济 | 怀特的异方差一致估计量
🎯要点 🎯算法和模型底层数学及代码:🖊线性代数应用(主成分分析):降维、投影(用于求解线性系统)和二次形式(用于优化)| 🖊奇值分解…...
《被讨厌的勇气》笔记
自由就是被别人讨厌。对人而言,最大的不幸就是不喜欢自己。活在“如果怎样怎样”之类的假设之中,就根本无法改变。活在害怕关系破裂的恐惧之中,那是为他人而活的一种不自由的生活方式。人生是连续刹那,我们只能活在“此时此刻”。…...

Python爬虫协程批量下载图片
import aiofiles import aiohttp import asyncio import requests from lxml import etree from aiohttp import TCPConnectorclass Spider:def __init__(self, value):# 起始urlself.start_url value# 下载单个图片staticmethodasync def download_one(url):name url[0].spl…...
Flask Web开发基础:数据库与ORM实战
Flask Web开发基础:数据库与ORM实战 该文介绍了如何使用 Flask、SQLAlchemy 和 SQLite 实现数据库操作。首先,通过创建虚拟环境和安装 flask-sqlalchemy(版本2.5.1)及 sqlalchemy(版本1.4.47)来设置环境。接…...

pidstat -d 1分析磁盘吞吐量
iostat -dx 1 查看磁盘IO吞吐量 pidstat -d 1看是哪个进程写的...

期望20K,2年golang深圳某互联网小公司一面
后续约了二面(CTO面),需要到现场,基本没问啥具体的技术知识,都是聊规划和个人职业目标 一面 1、假设访问百度网站,从在浏览器输入网址,到最终页面展示出来,中间会发生哪些事情&…...

#02 安装指南:如何配置Stable Diffusion环境
文章目录 前言前置条件第1步:安装Python和PIP第2步:创建虚拟环境第3步:安装PyTorch和CUDA第4步:安装Stable Diffusion相关库第5步:测试环境结论 前言 在之前的文章中,我们介绍了Stable Diffusion基础入门和…...
拼多多笔试
拼多多2022数据分析笔试(0822) 一、选择题 1.已知样本量n,样本均值及方差求置信区间 2.决策树 3.峰度系数 4.协方差 5.第一、第二熵变 6.充分统计量 7.xgboost 8.方差分析中的多重比较 二、编程题 1. 一张用户点击路径的表&#x…...

Golang | Leetcode Golang题解之第119题杨辉三角II
题目: 题解: func getRow(rowIndex int) []int {row : make([]int, rowIndex1)row[0] 1for i : 1; i < rowIndex; i {row[i] row[i-1] * (rowIndex - i 1) / i}return row }...
Flutter 中的 SliverIgnorePointer 小部件:全面指南
Flutter 中的 SliverIgnorePointer 小部件:全面指南 Flutter 是一个由 Google 开发的跨平台 UI 框架,它提供了一系列的组件来帮助开发者构建高性能、美观的移动、Web 和桌面应用。在 Flutter 的滚动组件中,SliverIgnorePointer 是一个用来包…...
比较两台计算机上的LabVIEW、工具包及驱动程序的一致性
比较两台计算机上的LabVIEW、工具包及驱动程序是否相同,可以通过以下步骤实现: 1. 检查LabVIEW版本 方法一:在LabVIEW中查看版本信息 步骤: 打开LabVIEW。点击菜单栏的 Help > About LabVIEW。记录显示的LabVIEW版本号和许可…...
参考——温湿度传感器DHT11驱动_STM32
设备:stm32f407ZGT6 环境:FreeRTOS HAL 到网上找DHT11的驱动,但是都无法使用。原因是RTOS环境中,由于多线程,使用循环计数阻塞式的delay_us延时函数就没那么准,且不同设备中delay_us的计数值不一样…...
架构每日一学 14:架构师如何进行可行性探索?
架构活动中,如果不进行可行性探索可能会导致重大失误,为企业发展带来风险。 可行性探索是架构活动的最后一个节点,在这之后的架构活动就像是离弦之箭,即便发现重大风险也很难再回头了。 互联网公司之间的竞争非常激烈࿰…...
多线程知识-13
为什么应该在循环中检查等待条件 为了实现多线程的同步和协调,通常使用等待和唤醒机制。在等待和唤醒机制中,等待条件是指一个线程等待某个条件的满足,当条件满足时,线程被唤醒继续执行。 在循环中检查等待条件的目的是为了避免虚…...
vue3+cli-service配置代理,跨域请求
一、配置代理端口和代理转发 在vue.config.js文件中 const {defineConfig} require(vue/cli-service)module.exports defineConfig({devServer: {host: 0.0.0.0,port: 8088, // 启动端口号proxy: {/api: { // 请求接口中要替换的标识target: , // 代理地址,后…...
git介绍、安装、配置
文章目录 1. GIT介绍2. 使用GIT的好处3. GIT 安装4. GIT 配置4.1 GIT 初始化设置、命令别名设置4.2 如果终端安装了oh-my-zsh,会带一堆git命令别名4.3 GIT配置文件介绍4.3.1 Linux、Mac OS系统4.3.2 windows系统 5. git设置远程仓库账号密码(拉取、上传代码不用输入…...
打开flutter调试
debugPaintSizeEnabled true; debugPaintBaselinesEnabled true;...

【前端 - Vue】Vuex基础入门,创建仓库的详细步骤
🚀 个人简介:6年开发经验,现任职某国企前端负责人,分享前端相关技术与工作常见问题~ 💟 作 者:前端菜鸟的自我修养❣️ 📝 专 栏:vue从基础到起飞 🌈 若有帮助&…...

#01 Stable Diffusion基础入门:了解AI图像生成
文章目录 前言什么是Stable Diffusion?Stable Diffusion的工作原理如何使用Stable Diffusion?Stable Diffusion的应用场景结论 前言 在当今迅速发展的人工智能领域,AI图像生成技术以其独特的魅力吸引了广泛的关注。Stable Diffusion作为其中的一项前沿技术&#…...

大数据学习栈记——Neo4j的安装与使用
本文介绍图数据库Neofj的安装与使用,操作系统:Ubuntu24.04,Neofj版本:2025.04.0。 Apt安装 Neofj可以进行官网安装:Neo4j Deployment Center - Graph Database & Analytics 我这里安装是添加软件源的方法 最新版…...

智能仓储的未来:自动化、AI与数据分析如何重塑物流中心
当仓库学会“思考”,物流的终极形态正在诞生 想象这样的场景: 凌晨3点,某物流中心灯火通明却空无一人。AGV机器人集群根据实时订单动态规划路径;AI视觉系统在0.1秒内扫描包裹信息;数字孪生平台正模拟次日峰值流量压力…...

【开发技术】.Net使用FFmpeg视频特定帧上绘制内容
目录 一、目的 二、解决方案 2.1 什么是FFmpeg 2.2 FFmpeg主要功能 2.3 使用Xabe.FFmpeg调用FFmpeg功能 2.4 使用 FFmpeg 的 drawbox 滤镜来绘制 ROI 三、总结 一、目的 当前市场上有很多目标检测智能识别的相关算法,当前调用一个医疗行业的AI识别算法后返回…...

优选算法第十二讲:队列 + 宽搜 优先级队列
优选算法第十二讲:队列 宽搜 && 优先级队列 1.N叉树的层序遍历2.二叉树的锯齿型层序遍历3.二叉树最大宽度4.在每个树行中找最大值5.优先级队列 -- 最后一块石头的重量6.数据流中的第K大元素7.前K个高频单词8.数据流的中位数 1.N叉树的层序遍历 2.二叉树的锯…...

以光量子为例,详解量子获取方式
光量子技术获取量子比特可在室温下进行。该方式有望通过与名为硅光子学(silicon photonics)的光波导(optical waveguide)芯片制造技术和光纤等光通信技术相结合来实现量子计算机。量子力学中,光既是波又是粒子。光子本…...

HarmonyOS运动开发:如何用mpchart绘制运动配速图表
##鸿蒙核心技术##运动开发##Sensor Service Kit(传感器服务)# 前言 在运动类应用中,运动数据的可视化是提升用户体验的重要环节。通过直观的图表展示运动过程中的关键数据,如配速、距离、卡路里消耗等,用户可以更清晰…...

嵌入式学习笔记DAY33(网络编程——TCP)
一、网络架构 C/S (client/server 客户端/服务器):由客户端和服务器端两个部分组成。客户端通常是用户使用的应用程序,负责提供用户界面和交互逻辑 ,接收用户输入,向服务器发送请求,并展示服务…...

招商蛇口 | 执笔CID,启幕低密生活新境
作为中国城市生长的力量,招商蛇口以“美好生活承载者”为使命,深耕全球111座城市,以央企担当匠造时代理想人居。从深圳湾的开拓基因到西安高新CID的战略落子,招商蛇口始终与城市发展同频共振,以建筑诠释对土地与生活的…...

【 java 虚拟机知识 第一篇 】
目录 1.内存模型 1.1.JVM内存模型的介绍 1.2.堆和栈的区别 1.3.栈的存储细节 1.4.堆的部分 1.5.程序计数器的作用 1.6.方法区的内容 1.7.字符串池 1.8.引用类型 1.9.内存泄漏与内存溢出 1.10.会出现内存溢出的结构 1.内存模型 1.1.JVM内存模型的介绍 内存模型主要分…...

快速排序算法改进:随机快排-荷兰国旗划分详解
随机快速排序-荷兰国旗划分算法详解 一、基础知识回顾1.1 快速排序简介1.2 荷兰国旗问题 二、随机快排 - 荷兰国旗划分原理2.1 随机化枢轴选择2.2 荷兰国旗划分过程2.3 结合随机快排与荷兰国旗划分 三、代码实现3.1 Python实现3.2 Java实现3.3 C实现 四、性能分析4.1 时间复杂度…...