Unity自定义后处理——Tonemapping色调映射
大家好,我是阿赵。
继续介绍屏幕后处理,这一期介绍一下Tonemapping色调映射
一、Tone Mapping的介绍
Tone Mapping色调映射,是一种颜色的映射关系处理,简单一点说,一般是从原始色调(通常是高动态范围,HDR)映射到目标色调(通常是低动态范围,LDR)。
由于HDR的颜色值是能超过1的,但实际上在LDR范围,颜色值最大只能是1。如果我们要在LDR的环境下,尽量模拟HDR的效果,超过1的颜色部分怎么办呢?
最直接想到的是两种可能:
1、截断大于1的部分
大于1的部分,直接等于1,小于1的部分保留。这种做法,会导致超过1的部分全部变成白色,在原始图片亮度比较高的情况下,转换完之后可能就是一片白茫茫的效果。
2、对颜色进行线性的缩放
把原始颜色的最大值看做1,然后把原始的所有颜色进行整体的等比缩放。这样做,能保留一定的效果。但由于原始的HDR颜色的跨度可能比0到1大很多,所以整体缩小之后,整个画面就会变暗很多了,没有了HDR的通透光亮的感觉。
为了能让HDR颜色映射到LDR之后,还能保持比较接近的效果,上面两种方式的处理显然都是不好的。
Tonemapping也是把HDR颜色范围映射到0-1的LDR颜色范围,但它并不是线性缩放,而是曲线的缩放。

从上面这个例子可以看出来,Tonemapping映射之后的颜色,有些地方是变暗了,比如深颜色的裤子,但有些地方却是变亮了的,比如头发和肩膀衣服上的阴影。整体的颜色有一种电影校色之后的感觉。
很多游戏美工在没有技术人员配合的情况下,都很喜欢自己挂后处理,其中Tonemapping应该是除了Bloom以外,美工们最喜欢的一种后处理了,虽然不知道为什么,但就是觉得颜色好看了。
虽然屏幕后处理看着好像很简单实现,挂个组件调几个参数,就能化腐朽为神奇,把原本平淡无奇的画面变得好看。但其实后处理都是有各种额外消耗的,所以我一直不是很建议美工们只会依靠后处理来扭转画面缺陷的,特别是做手游。
二、Tonemapping的代码实现
1、C#代码
using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class TonemappingCtrl : MonoBehaviour
{private Material toneMat;public bool isTonemapping = false;// Start is called before the first frame updatevoid Start(){}// Update is called once per framevoid Update(){}private bool TonemappingFun(RenderTexture source, RenderTexture destination){if (toneMat == null){toneMat = new Material(Shader.Find("Hidden/ToneMapping"));}if (toneMat == null || toneMat.shader == null || toneMat.shader.isSupported == false){return false;}Graphics.Blit(source, destination, toneMat);return true;}private void OnRenderImage(RenderTexture source, RenderTexture destination){if(isTonemapping == false){Graphics.Blit(source, destination);return;}RenderTexture finalRt = source;if (TonemappingFun(finalRt,finalRt)==false){Graphics.Blit(source, destination);}else{Graphics.Blit(finalRt, destination);}}
}
C#部分的代码和其他后处理没区别,都是通过OnRenderImage里面调用Graphics.Blit
2、Shader
Shader "Hidden/ToneMapping"
{Properties{_MainTex ("Texture", 2D) = "white" {}}SubShader{// No culling or depthCull Off ZWrite Off ZTest AlwaysPass{CGPROGRAM#pragma vertex vert_img#pragma fragment frag#include "UnityCG.cginc"sampler2D _MainTex; float3 ACES_Tonemapping(float3 x){float a = 2.51f;float b = 0.03f;float c = 2.43f;float d = 0.59f;float e = 0.14f;float3 encode_color = saturate((x*(a*x + b)) / (x*(c*x + d) + e));return encode_color;}fixed4 frag (v2f_img i) : SV_Target{fixed4 col = tex2D(_MainTex, i.uv);half3 linear_color = pow(col.rgb, 2.2);half3 encode_color = ACES_Tonemapping(linear_color);col.rgb = pow(encode_color, 1 / 2.2);return col;}ENDCG}}
}
需要说明一下:
1.色彩空间的转换
由于默认显示空间是Gamma空间,所以先通过pow(col.rgb, 2.2)把颜色转换成线性空间,然后再进行Tonemapping映射,最后再pow(encode_color, 1 / 2.2),把颜色转回Gamma空间
2.Tonemapping映射算法
float3 ACES_Tonemapping(float3 x){float a = 2.51f;float b = 0.03f;float c = 2.43f;float d = 0.59f;float e = 0.14f;float3 encode_color = saturate((x*(a*x + b)) / (x*(c*x + d) + e));return encode_color;}
把颜色进行Tonemapping映射。这个算法是网上都可以百度得到的。
三、Tonemapping和其他后处理的配合
一般来说,Tonemapping只是一个固定颜色映射效果,所以应该是需要配合着其他的效果一起使用,才会得到比较好的效果。比如我之前介绍过的校色、暗角、Bloom等。


可以做出各种不同的效果,不同于原始颜色的平淡,调整完之后的颜色看起来会比较有电影的感觉。
这也是我为什么要在Unity有PostProcessing后处理插件的情况下,还要介绍使用自己写Shader实现屏幕后处理的原因。PostProcessing作为一个插件,它可能会存在很多功能,会有很多额外的计算,你可能只需要用到其中的某一个小部分的功能和效果。
而我们自己写Shader实现屏幕后处理,自由度非常的高,喜欢在哪里添加或者修改一些效果,都可以。
比如,我可以写一个脚本,把之前介绍过的所有后处理效果都加进去:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
//[ExecuteInEditMode]
public class ImageEffectCtrl : MonoBehaviour
{//--------调色-----------private Material colorMat;public bool isColorAjust = false;[Range(-5,5)]public float saturation = 1;[Range(-5,5)]public float contrast = 1;[Range(0,1)]public float hueShift = 0;[Range(0,5)]public float lightVal = 1;[Range(0,3)]public float vignetteIntensity = 1.8f;[Range(0,5)]public float vignetteSmoothness = 5;//-------模糊-----------private Material blurMat;public bool isBlur = false;[Range(0, 4)]public float blurSize = 0;[Range(-3,3)]public float blurOffset = 1;[Range(1,3)]public int blurType = 3;//-----光晕----------private Material brightMat;private Material bloomMat;public bool isBloom = false;[Range(0,1)]public float brightCut = 0.5f;[Range(0, 4)]public float bloomSize = 0;[Range(-3, 3)]public float bloomOffset = 1;public int bloomType = 3;[Range(1, 3)]//---toneMapping-----private Material toneMat;public bool isTonemapping = false;// Start is called before the first frame updatevoid Start(){//if(colorMat == null||colorMat.shader == null||colorMat.shader.isSupported == false)//{// this.enabled = false;//}}// Update is called once per framevoid Update(){}private bool AjustColor(RenderTexture source, RenderTexture destination){if(colorMat == null){colorMat = new Material(Shader.Find("Hidden/AzhaoAjustColor"));}if(colorMat == null||colorMat.shader == null||colorMat.shader.isSupported == false){return false;}colorMat.SetFloat("_Saturation", saturation);colorMat.SetFloat("_Contrast", contrast);colorMat.SetFloat("_HueShift", hueShift);colorMat.SetFloat("_Light", lightVal);colorMat.SetFloat("_VignetteIntensity", vignetteIntensity);colorMat.SetFloat("_VignetteSmoothness", vignetteSmoothness);Graphics.Blit(source, destination, colorMat, 0);return true;}private Material GetBlurMat(int bType){if(bType == 1){return new Material(Shader.Find("Hidden/AzhaoBoxBlur"));}else if(bType == 2){return new Material(Shader.Find("Hidden/AzhaoGaussianBlur"));}else if(bType == 3){return new Material(Shader.Find("Hidden/AzhaoKawaseBlur"));}else{return null;}}private bool CheckNeedCreateBlurMat(Material mat,int bType){if(mat == null){return true;}if(mat.shader == null){return true;}if(bType == 1){if(mat.shader.name != "Hidden/AzhaoBoxBlur"){return true;}else{return false;}}else if(bType == 2){if (mat.shader.name != "Hidden/AzhaoGaussianBlur"){return true;}else{return false;}}else if (bType == 3){if (mat.shader.name != "Hidden/AzhaoKawaseBlur"){return true;}else{return false;}}else{return false;}}private bool BlurFun(RenderTexture source, RenderTexture destination,float blurTime,int bType,float offset ){if(CheckNeedCreateBlurMat(blurMat,bType)==true){blurMat = GetBlurMat(bType);}if (blurMat == null || blurMat.shader == null || blurMat.shader.isSupported == false){return false;}blurMat.SetFloat("_BlurOffset", offset);float width = source.width;float height = source.height;int w = Mathf.FloorToInt(width);int h = Mathf.FloorToInt(height);RenderTexture rt1 = RenderTexture.GetTemporary(w, h);RenderTexture rt2 = RenderTexture.GetTemporary(w, h);Graphics.Blit(source, rt1);for (int i = 0; i < blurTime; i++){ReleaseRT(rt2);width = width / 2;height = height / 2;w = Mathf.FloorToInt(width);h = Mathf.FloorToInt(height);rt2 = RenderTexture.GetTemporary(w, h);Graphics.Blit(rt1, rt2, blurMat, 0);width = width / 2;height = height / 2;w = Mathf.FloorToInt(width);h = Mathf.FloorToInt(height);ReleaseRT(rt1);rt1 = RenderTexture.GetTemporary(w, h);Graphics.Blit(rt2, rt1, blurMat, 1);}for (int i = 0; i < blurTime; i++){ReleaseRT(rt2);width = width * 2;height = height * 2;w = Mathf.FloorToInt(width);h = Mathf.FloorToInt(height);rt2 = RenderTexture.GetTemporary(w, h);Graphics.Blit(rt1, rt2, blurMat, 0);width = width * 2;height = height * 2;w = Mathf.FloorToInt(width);h = Mathf.FloorToInt(height);ReleaseRT(rt1);rt1 = RenderTexture.GetTemporary(w, h);Graphics.Blit(rt2, rt1, blurMat, 1);}Graphics.Blit(rt1, destination);ReleaseRT(rt1);rt1 = null;ReleaseRT(rt2);rt2 = null;return true;}private bool BrightRangeFun(RenderTexture source, RenderTexture destination){if(brightMat == null){brightMat = new Material(Shader.Find("Hidden/BrightRange"));}if (brightMat == null || brightMat.shader == null || brightMat.shader.isSupported == false){return false;}brightMat.SetFloat("_BrightCut", brightCut);Graphics.Blit(source, destination, brightMat);return true;}private bool BloomAddFun(RenderTexture source,RenderTexture destination, RenderTexture brightTex){if(bloomMat == null){bloomMat = new Material(Shader.Find("Hidden/AzhaoBloom"));}if (bloomMat == null || bloomMat.shader == null || bloomMat.shader.isSupported == false){return false;}bloomMat.SetTexture("_brightTex", brightTex);Graphics.Blit(source, destination, bloomMat);return true;}private bool TonemappingFun(RenderTexture source, RenderTexture destination){if(toneMat == null){toneMat = new Material(Shader.Find("Hidden/ToneMapping"));}if (toneMat == null || toneMat.shader == null || toneMat.shader.isSupported == false){return false;}Graphics.Blit(source, destination, toneMat);return true;}private void CopyRender(RenderTexture source,RenderTexture destination){Graphics.Blit(source, destination);}private void ReleaseRT(RenderTexture rt){if(rt!=null){RenderTexture.ReleaseTemporary(rt);}}private void OnRenderImage(RenderTexture source, RenderTexture destination){ RenderTexture finalRt = source;RenderTexture rt2 = RenderTexture.GetTemporary(source.width, source.height);RenderTexture rt3 = RenderTexture.GetTemporary(source.width, source.height);if (isBloom == true){if(BrightRangeFun(finalRt, rt2)==true){if(BlurFun(rt2, rt3, bloomSize,bloomType,bloomOffset)==true){if(BloomAddFun(source, finalRt, rt3)==true){} }}}if(isBlur == true){if (blurSize > 0){if (BlurFun(finalRt, finalRt, blurSize,blurType,blurOffset) == true){}}}if (isTonemapping == true){if (TonemappingFun(finalRt, finalRt) == true){}}if (isColorAjust == true){ if (AjustColor(finalRt, finalRt) == true){}}CopyRender(finalRt, destination);ReleaseRT(finalRt);ReleaseRT(rt2);ReleaseRT(rt3);}
}

一个脚本控制所有后处理。当然这样的做法只是方便,也不见得很好,我还是比较喜欢根据实际用到多少个效果,单独去写对应的脚本,那样我觉得性能才是最好的。
相关文章:
Unity自定义后处理——Tonemapping色调映射
大家好,我是阿赵。 继续介绍屏幕后处理,这一期介绍一下Tonemapping色调映射 一、Tone Mapping的介绍 Tone Mapping色调映射,是一种颜色的映射关系处理,简单一点说,一般是从原始色调(通常是高动态范围&…...
Redis学习 知识总结 一
Redis学习 知识总结 一 1 Redis初识1.1 Redis八大特性1.2 redis使用场景1.3 Docker安装redis 2 API的理解和使用2.1 通用命令2.2 字符串(String)类型2.3 哈希(Hash)类型2.4 有序列表(list)2.5 集合…...
Webpack5 vue-loader和VueLoaderPlugin
文章目录 vue-loader和VueLoaderPlugin的作用vue-loader具体使用方式注意事项 vue-loader和VueLoaderPlugin的作用 .vue 文件是用户用 HTML-like 的语法编写的 Vue 组件。每个vue 文件都包括三部分 , VueLoaderPlugin 是一个解析 Vue.js 的插件,用于在 webpack 构…...
【传统视觉】模板匹配和卡尺圆检测
模板匹配 粗定位 1、原理:模板匹配是指在当前图像A中匹配与图像B最相似的部分,那么A为输入图像,B为模板图像。 2、匹配方法:B在A上华东,逐个遍历所有像素完成匹配。 3、函数: result cv2.matchTemplate(…...
记一次简单的MySql注入试验
试验环境: 1.已经搭建好的php服务器,并可以通过访问到localhost/index.php; 2.已经安装好数据库,并创建表test,表内有name、age等字段,并随便创建几个假数据用于测试;如图: 开始测…...
软考开发思考(完善中)
软考开发思考 文章目录 软考开发思考1. 互联网媒体:新技术和新应用及当前的趋势和应用1.1 自动化报道1.2. 虚拟和增强现实1.3. 数据新闻1.4. 即时新闻推送1.5 智能助手和聊天机器人1.6 语音播报,语音检索,后台播放、播放倍速。1.6 机器人交互…...
[NLP]LLaMA与LLamMA2解读
摘要 Meta最近提出了LLaMA(开放和高效的基础语言模型)模型参数包括从7B到65B等多个版本。最值得注意的是,LLaMA-13B的性能优于GPT-3,而体积却小了10倍以上,LLaMA-65B与Chinchilla-70B和PaLM-540B具有竞争性。 一、引言 一般而言࿰…...
后端一次返回大量数据,前端做分页处理
问题描述:后端接口返回大量数据,没有做分页处理,不支持传参pageNum,pageSize 本文为转载文章,原文章:后端一次返回大量数据,前端做分页处理 1.template中 分页 <el-paginationsize-chang…...
卷积神经网络识别人脸项目—使用百度飞桨ai计算
卷积神经网络识别人脸项目的详细过程 整个项目需要的准备文件: 下载链接: 链接:https://pan.baidu.com/s/1WEndfi14EhVh-8Vvt62I_w 提取码:7777 链接:https://pan.baidu.com/s/10weqx3r_zbS5gNEq-xGrzg 提取码&#x…...
vue中预览静态pdf文件
方法 // pdf预览 viewFileCompare() { const pdfUrl "/static/wjbd.pdf"; window.open(pdfUrl); }, // 下载 downloadFile(){ var a document.createElement("a"); a.href "/static/wjbd.pdf"; a.…...
Java多进程(详细)
进程的含义 简单理解是正在跑起来的程序,正在运行的程序。没有正在运行的程序不叫进程,同一个程序,运行多次,就可能产生多个进程。 平时所说的程序,值的是一些exe的可执行文件,得把程序跑起来才会涉及到进程…...
OpenCV 4.0+Python机器学习与计算机视觉实战
💂 个人网站:【办公神器】【游戏大全】【神级源码资源网】🤟 前端学习课程:👉【28个案例趣学前端】【400个JS面试题】💅 寻找学习交流、摸鱼划水的小伙伴,请点击【摸鱼学习交流群】 目录 前言第一部分&…...
自学网络安全(黑客)全笔记
一、什么是网络安全 网络安全可以基于攻击和防御视角来分类,我们经常听到的 “红队”、“渗透测试” 等就是研究攻击技术,而“蓝队”、“安全运营”、“安全运维”则研究防御技术。 无论网络、Web、移动、桌面、云等哪个领域,都有攻与防两面…...
WAF/Web应用安全(拦截恶意非法请求)
Web 应用防火墙(Web Application Firewall, WAF)通过对 HTTP(S) 请求进行检测,识别并阻断 SQL 注入、跨站脚本攻击、跨站请求伪造等攻击,保护 Web 服务安全稳定。 Web 安全是所有互联网应用必须具备的功能,…...
Windows环境下git客户端中的git-bash和MinGW64
我们在 Windows10 操作系统下,安装了 git 客户端之后,可以通过 git-bash.exe 打开一个 shell: 执行一些 linux 系统里的命令: 注意到上图紫色的 MINGW64. Mingw-w64 是原始 mingw.org 项目的改进版,旨在支持 Window…...
欧姆龙CX系列PLC串口转以太网欧姆龙cp1hplc以太网连接电脑
你是否还在为工厂设备信息采集困难而烦恼?捷米特JM-ETH-CX转以太网通讯处理器为你解决这个问题! 捷米特JM-ETH-CX转以太网通讯处理器专门为满足工厂设备信息化需求而设计,可以用于欧姆龙多个系列PLC的太网数据采集,非常方便构建生…...
Vue3笔记
1. Vue2 选项式 API vs Vue3 组合式API <script> export default { data(){ return { count:0 } }, methods:{ addCount(){ this.count } } } </script> <script setup> import { ref } from vue const count ref(0) c…...
git相关
gerrit用户指南: 资料:Gerrit 用户指南 gerrit-user-guide 上述有介绍如何review,review并非修改代码之后如何重新提交等操作 jenkins介绍 Jenkins详细教程 - 知乎 一、jenkins是什么? Jenkins是一个开源的、提供友好操作界…...
车道线检测|利用边缘检测的原理对车道线图片进行识别
前言 那么这里博主先安利一些干货满满的专栏了! 这两个都是博主在学习Linux操作系统过程中的记录,希望对大家的学习有帮助! 操作系统Operating Syshttps://blog.csdn.net/yu_cblog/category_12165502.html?spm1001.2014.3001.5482Linux S…...
C++—static关键字详解
引言: C的static有两种用法:面向过程程序设计中的static和面向对象程序设计中的static。前者应用于普通变量和函数,不涉及类;后者主要说明static在类中的作用。 一.面向过程中的static 1.静态全局变量 静态全局变量有以下特点…...
k8s从入门到放弃之Ingress七层负载
k8s从入门到放弃之Ingress七层负载 在Kubernetes(简称K8s)中,Ingress是一个API对象,它允许你定义如何从集群外部访问集群内部的服务。Ingress可以提供负载均衡、SSL终结和基于名称的虚拟主机等功能。通过Ingress,你可…...
《基于Apache Flink的流处理》笔记
思维导图 1-3 章 4-7章 8-11 章 参考资料 源码: https://github.com/streaming-with-flink 博客 https://flink.apache.org/bloghttps://www.ververica.com/blog 聚会及会议 https://flink-forward.orghttps://www.meetup.com/topics/apache-flink https://n…...
【C++从零实现Json-Rpc框架】第六弹 —— 服务端模块划分
一、项目背景回顾 前五弹完成了Json-Rpc协议解析、请求处理、客户端调用等基础模块搭建。 本弹重点聚焦于服务端的模块划分与架构设计,提升代码结构的可维护性与扩展性。 二、服务端模块设计目标 高内聚低耦合:各模块职责清晰,便于独立开发…...
在Mathematica中实现Newton-Raphson迭代的收敛时间算法(一般三次多项式)
考察一般的三次多项式,以r为参数: p[z_, r_] : z^3 (r - 1) z - r; roots[r_] : z /. Solve[p[z, r] 0, z]; 此多项式的根为: 尽管看起来这个多项式是特殊的,其实一般的三次多项式都是可以通过线性变换化为这个形式…...
08. C#入门系列【类的基本概念】:开启编程世界的奇妙冒险
C#入门系列【类的基本概念】:开启编程世界的奇妙冒险 嘿,各位编程小白探险家!欢迎来到 C# 的奇幻大陆!今天咱们要深入探索这片大陆上至关重要的 “建筑”—— 类!别害怕,跟着我,保准让你轻松搞…...
在鸿蒙HarmonyOS 5中使用DevEco Studio实现指南针功能
指南针功能是许多位置服务应用的基础功能之一。下面我将详细介绍如何在HarmonyOS 5中使用DevEco Studio实现指南针功能。 1. 开发环境准备 确保已安装DevEco Studio 3.1或更高版本确保项目使用的是HarmonyOS 5.0 SDK在项目的module.json5中配置必要的权限 2. 权限配置 在mo…...
深入解析光敏传感技术:嵌入式仿真平台如何重塑电子工程教学
一、光敏传感技术的物理本质与系统级实现挑战 光敏电阻作为经典的光电传感器件,其工作原理根植于半导体材料的光电导效应。当入射光子能量超过材料带隙宽度时,价带电子受激发跃迁至导带,形成电子-空穴对,导致材料电导率显著提升。…...
LUA+Reids实现库存秒杀预扣减 记录流水 以及自己的思考
目录 lua脚本 记录流水 记录流水的作用 流水什么时候删除 我们在做库存扣减的时候,显示基于Lua脚本和Redis实现的预扣减 这样可以在秒杀扣减的时候保证操作的原子性和高效性 lua脚本 // ... 已有代码 ...Overridepublic InventoryResponse decrease(Inventor…...
高保真组件库:开关
一:制作关状态 拖入一个矩形作为关闭的底色:44 x 22,填充灰色CCCCCC,圆角23,边框宽度0,文本为”关“,右对齐,边距2,2,6,2,文本颜色白色FFFFFF。 拖拽一个椭圆,尺寸18 x 18,边框为0。3. 全选转为动态面板状态1命名为”关“。 二:制作开状态 复制关状态并命名为”开…...
比特币:固若金汤的数字堡垒与它的四道防线
第一道防线:机密信函——无法破解的哈希加密 将每一笔比特币交易比作一封在堡垒内部传递的机密信函。 解释“哈希”(Hashing)就是一种军事级的加密术(SHA-256),能将信函内容(交易细节…...
