尝试打包WebGL平台遇到的坑

Intro

在将一个 Unity 2022.3 数字孪生项目(利用Unity3D_Robotics_ABB)从原 Win 平台迁移到 WebGL 平台的过程中,遇到了 RuntimeError: null function or function signature mismatchThe script 'xxxxx' could not be instantiated!digest auth failed 等等报错和故障。在对网上的解决方案探索和尝试的过程中,有小结如下

System.Threading 相关命名空间(C# 原生线程)不支持

这是着手切换WebGL平台之前就知道不行的一点,不过使用 UniTask 可以替换。

Unity3D_Robotics_ABB 作者在其实现中主要使用了原生的 Thread 来处理异步的逻辑,在 Unity 打 WebGL 包时并没有报错且能成功出包,然而,在运行 WebGL 的程序时(尤其做某些会拉起线程的操作时),会出现卡死、弹窗报错RuntimeError: null function or function signature mismatch 的问题。

若出现上述问题,应该考虑利用 Unity 托管的异步实现(UniTask、协程等),或 async/await 关键词处理对应需求。

System.Net 相关命名空间不支持(需要利用Unity提供的 UnityWebRequest(UWR) 来进行 HTTP 请求)

这是解决问题花费时间比较长的一点。

在解决完线程的问题后,运行 WebGL 程序,发现会出现 The script 'xxxxx' could not be instantiated! 等控制台警告,且此脚本功能失效。经过测试后,发现脚本中若引用了 System.Net 等 .NET networking classes,则此脚本不能被正确加载,见 WebGL Networking ,此时需要使用 UnityWebRequest 类进行 http 请求。

由于 UnityWebRequest 没有对 Authentication 的支持,需要手动处理一些问题:digest auth failed 。例如,Basic 验证方案可以自己加Header 并存入对应值。然而,对于 Digest 验证方案,手动处理比较麻烦。此时考虑调 JavaScript 的加 Digest 验证的方法并发送 HTTP 请求,或者由代理服务器添加 Digest 验证

技术水平有限,最终选择了由代理服务器添加 Digest 验证的方法,下面是对应的 Python 程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
from flask import Flask, request, Response
import requests
from requests.auth import HTTPDigestAuth

app = Flask(__name__)

# The URL of the API that requires Digest Authentication
TARGET_API_URL = "http://www.tsingloo.com"
# Replace 'your_username' and 'your_password' with your actual credentials
USERNAME = 'your_username'
PASSWORD = 'your_password'

@app.route('/', defaults={'path': ''})
@app.route('/<path:path>', methods=['GET', 'POST', 'PUT', 'DELETE'])
def proxy(path):
# Construct the full URL to the target API
url = f"{TARGET_API_URL}/{path}"

# Log the requested URL
app.logger.info(f"I am requesting {url}")

# Get the original request method and data
method = request.method
data = request.get_data()

# Selectively copy headers, excluding those that can cause issues
excluded_headers = ['Host', 'Content-Length', 'Content-Type']
headers = {key: value for key, value in request.headers if key not in excluded_headers}

# If the request has a content type, include it in the headers
if 'Content-Type' in request.headers:
headers['Content-Type'] = request.headers['Content-Type']

# Inside your proxy function
response = requests.request(method, url, data=data, headers=headers)

# Make sure to copy the content-type header from the original response to the new response
content_type = response.headers.get('Content-Type')
response_headers = {'Content-Type': content_type} if content_type else {}

return Response(response.content, status=response.status_code, headers=response_headers)

@app.before_request
def log_request():
print(f"Received request: {request.url} - {request.method}")

if __name__ == '__main__':
app.run(debug=True)

短时间内利用 UWR 对 RWS 发送大量请求

为了追求项目的实时性,短时间内 Unity 利用 UWR 向机械臂(RWS)发送大量请求,前70个请求成功,而后的请求报 “503 Service Unavailable (Too many sessions 70/70)”错,且当双方断开连接时,仍需要一段时间“冷却”,而后才能请求正确收到回复,然后短时间内约70个 UWR 的 http 的请求后,重复上述 503 错误。使用C#提供的 HTTP 相关类时未见此问题。当503报错时,关闭代理服务器,重新插拔网线,都不能立刻释放此70个sessions。

值得注意的是,当 UWR 已经收到503报错时,再继续使用Postman测试相关接口,如(http://192.168.x.x/rw/rapid/tasks/T_ROB1/motion?resource=jointtarget)时,Postman有时可以正常收到数据。复盘此情况,推测是在排障中,由 Postman 已经发送了带 http头”Connection: Keep-Alive”并成功建立了正常的连接,早于 UWR 的请求,换言之,其 70 个session中已经维护了一个与postman的长连接。冷却后,使用 Postman 短时间内大量测试相关接口可以持续获得正常的数据,未见503报错。

冷却后,当使用Edge浏览器短时间内大量访问接口,前70个请求成功,而后的也会503报错,表现与 UWR 一样,且观察浏览器发出的 HTTP 请求,其已携带”Connection: Keep-Alive”,不同的是,浏览器发出的 HTTP 带有很长的“User-Agent”头,这一点没弄明白。

冷却后,当使用 Python 对接口短时间内大量发送简单的http请求(不挟带请求头”Connection: Keep-Alive”)时,前70个请求成功,而后的也会503报错,表现与 UWR 一样。

经检查,(编辑器 2022.3 版本默认的) UWR 发出的 HTTP 请求默认不携带标头 Connection: Keep-Alive,有讨论,见Connection: keep-alive in Unity Web Request?,且当使用 UnityWebRequest.SetRequestHeader 手动设置此标头时,会见警告且官方文档不推荐如此做。

最终,通过 UnityWebRequest.SetRequestHeader 手动设置标头”Connection: Keep-Alive”,解决了上述 503 报错。

几乎同一时刻对 RWS 发送多个请求

在将原线程替换为 UniTask 后,某个方法每帧将同时对 RWS 发送三个不同的 HTTP 请求,导致报 “Connection manager can’t add .. ” 错,改为一帧(约23ms)轮流发一个 HTTP 请求后解决。

HttpClient.PostAsyncUnityWebRequest.Post

使用PostAsync时,需要传入一个string和一个httpcontent,在使用 .Post 时,可以是两个string,也可以是一个string和一个 WWWForm,推荐使用后者,可以规避一些可能的编码问题。

反序列化时 dynamic 关键字引起的崩溃

项目中使用从 Unity Package Manager 中添加的 Newtonsoft.Json 来处理请求,原本从字符串中反序列化对象代码如下:

1
2
dynamic obj = JsonConvert.DeserializeObject(jsonStr);
var state = obj.name;

初次尝试打WebGL包时会报缺少某命名空间错,而后在项目设置中将 API 兼容性 级别改为 .Net Framewrok 后不再报错。
上述代码会导致WebGL运行时弹窗报错奔溃,推荐参考 json 的内容定义类型而后获取对应字段的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[Serializable]
public class People
{
public string name;
public int age;
public DateTime birthday;
public bool isMarried;
public float money;
}

public void DeserializeJson()
{
string jsonStr = "{\"name\" :\"张三\",\"age\":30,\"birthday\":\"1990-03-31T17:15:29\",\"isMarried\":true,\"money\":10.0}";

People zhangsan = JsonConvert.DeserializeObject<People>(jsonStr);
Debug.Log("zhangsan.name " + zhangsan.name + " birthday: " + zhangsan.birthday + " isMarried: " + zhangsan.isMarried);

//output: zhangsan.name 张三 birthday: 03/31/1990 17:15:29 isMarried: True
}

QFramework 随笔

参考资料:

  1. Unity游戏框架搭建决定版 QFramework的二次开发
  2. “您好 我是QFramework”
  3. QFramework v1.0 使用指南
  4. 基础 | 三层架构与MVC模式

Introduction

QFramework.cs 提供了 MVC、分层、CQRS、事件驱动、数据驱动等工具,除了这些工具,QFramework.cs 还提供了架构使用规范。

QFramework 内置模块如下:

  • 0.Framework:核心架构(包含一套系统设计架构)

  • 1.CoreKit: 核心工具库、插件管理

  • 2.ResKit:资源管理套件(快速开发)

  • 3.UIKit:UI 管理套件(支持自动绑定、代码生成)

  • 4.Audio:音频方案

    MVC

    MVC模式是软件工程中常见的一种软件架构模式,该模式把软件系统(项目)分为三个基本部分:**模型(Model)、视图(View)和控制器(Controller)**使用此模式有诸多优势,例如:简化后期对项目的修改、扩展等维护操作;使项目的某一部分变得可以重复利用;使项目的结构更加直观。

    1. **视图(View):**负责界面的显示,以及与用户的交互功能。实际在Unity中,这一部分往往指 UI 的呈现。

    2. 控制器(Controller):可以理解为一个分发器,用来决定对于视图发来的请求(命令),需要用哪一个模型来处理,以及处理完后需要跳回(通过事件更改)到哪一个视图。即用来连接视图和模型。

    3. **模型(Model):**模型持有所有的数据、状态和程序逻辑。模型接受视图数据(的命令),并返回最终的处理结果(,触发事件)。

      img

CQRS 命令和查询责任分离

一种将数据存储的读取操作和更新操作分离的模式。

Query 是一个可选的概念,如果游戏中数据的查询逻辑并不是很重的话,直接在 Controller 的表现逻辑里写就可以了,但是查询数据比较重,或者项目规模非常大的话,最好是用 Query 来承担查询的逻辑。

img

事件驱动

在事件驱动编程中,系统的流程是由外部事件(如用户输入或外部数据更改)驱动的。程序会对发生的事件做出反应。其核心思想是定义并使用事件处理器

用户输入 => 事件响应 => 代码运行 => 刷新页面状态

InputSystem 帮助Unity开发者将用户输入抽象为事件

数据驱动

操作UI(用户输入)=> 触发事件 => 响应处理 => 更新数据 => 更新UI(呈现)

BindableProperty<T> 提供了快速的绑定

Framework

系统设计架构,核心概念包括Architecture、Command、Event、Model、System

Architecture

可以将Architecture视为一个项目模块的管理器(System)的集合, 省去创建大量零散管理器单例的麻烦

使用注册的方式将当前项目所使用的 模块 系统和 工具 添加进内部的IOC容器中,方便管理。

Controller

赋予 MonoBehaviour 脚本对象访问架构的能力

Model

同类的公共数据

架构规范与推荐用法

QFramework 架构提供了四个层级:

  • 表现层:IController
  • 系统层:ISystem
  • 数据层:IModel
  • 工具层:IUtility

通用规则

  • IController 更改 ISystem、IModel 的状态必须用Command
  • ISystem、IModel 状态发生变更后通知 IController 必须用事件或BindableProperty
  • IController可以获取ISystem、IModel对象来进行数据查询
  • ICommand、IQuery 不能有状态,
  • 上层可以直接获取下层,下层不能获取上层对象
  • 下层向上层通信用事件
  • 上层向下层通信用方法调用(只是做查询,状态变更用 Command),IController 的交互逻辑为特别情况,只能用 Command

表现层

ViewController 层。

IController接口,负责接收输入和状态变化时的表现,一般情况下,MonoBehaviour 均为表现层

  • 可以获取 System、Model
  • 可以发送 Command、Query
  • 可以监听 Event

系统层

System层

ISystem接口,帮助IController承担一部分逻辑,在多个表现层共享的逻辑,比如计时系统、商城系统、成就系统等

  • 可以获取 System、Model

  • 可以监听Event

  • 可以发送Event

数据层

IModel接口,负责数据的定义、数据的增删查改方法的提供

  • 可以获取 Utility
  • 可以发送 Event

工具层

Utility层

IUtility接口,负责提供基础设施,比如存储方法、序列化方法、网络连接方法、蓝牙方法、SDK、框架继承等。啥都干不了,可以集成第三方库,或者封装API

常用技巧

TypeEventSystem

独立的事件系统,可以理解为群发和广播,创建结构体传递事件内容

public struct PlayerDieEvent { }

一些简单的事件可以通过 .Register

WildPerception

WildPerception 是一个利用 Unity Perception Package 来生成大规模多视角视频数据集的工具。
其允许用户导入自己的 Humanoid 人物模型,或者利用 SyntheticHumans Package 以合成人物模型,从而在自定义的场景中模拟行人。配合 MultiviewX_Perception 可以得到符合 Wildtrack 格式的数据集。

注意:

  1. 原 MultiviewX_FYP 现更名为 MultiviewX_Perception
  2. CalibrateTool 已集成到了此处,将不再独立导出
  3. 开发使用的 Editor 版本为 2022.3.3f1,不保证之前版本(尤其是2022.2之前)的表现。 Unity Perception Package 要求一定版本的 HDRP 。

Support For SyntheticHumans

  1. 添加 WildPerception、 SyntheticHumans Package 到您的项目中,推荐导入 SyntheticHumans 官方提供的 Samples,具体过程请参考:Install Packages and Set Things Up

  2. 为 TsingLoo.WildPerception.asmdef 添加 AssemblyDefinitionReferences,选择 SyntheticHumans 提供的 Unity.CV.SyntheticHumans.Runtime,而后在页面底部右下角点击 Apply,保存。

    选择 SyntheticHumans 提供的 Unity.CV.SyntheticHumans.Runtime

    在页面底部右下角点击 Apply,保存

  3. 找到 RuntimeModelProvider.cs 脚本,更改 false 为 true

    更改 false 为 true

  4. 将 RuntimeModelProvider 组件分配给场景

    将 RuntimeModelProvider 组件分配给场景

  5. 移除其他的ModelProvider

    移除其他的ModelProvider

  6. 添加 HumanGenerationConfig,Config 的具体配置请参考此文档下半部分:Generate Your First Humans

添加 HumanGenerationConfig

  1. 运行

Import

有两种方法可以将 WildPerception 包添加到您的项目中。

注意:

  1. 由于需要安装相关依赖,应该在联网环境中导入此包

[Method 1] Add package from git URL

注意:

  1. 这需要您的设备有 git 环境,可以去这里安装:git
  2. 由于目前 Package Manager 的限制,若如此做不可打开示例场景 SampleScene_WildPerception,但不影响其他功能。

Unity Editor 中打开 Window -> Package Manager

Add package from git URL

复制本项目地址,填写并添加

复制本项目地址

填写并添加

大约三到五分钟后,添加完成,Unity 开始导入新包。

[Method 2] Add package from disk

通过 git clone 或者直接从 github 上下载ZIP文件,或者从此处[Download WildPerception] 下载包。

直接从 github 上下载ZIP文件

将此ZIP文件解压,放到非项目Assets文件夹中(在项目文件夹外亦可)

放到非项目Assets文件夹中

Unity Editor 中打开 Window -> Package Manager

Add package from disk

将package.json选中并确定。

将package.json选中

大约两分钟后,添加完成,Unity 开始导入新包。

Setup

您可以很快赋予您的场景生成序列帧的能力,只需要简单的几步配置。

若您使用的是方法2来添加此包,不妨打开场景 Packages -> WildPerception -> Sample -> SampleScene_WildPerception。 这个场景中比较简洁,如下图所示,有预制体SceneController 和一些GameObject

需要用到的预制体及生成的GameObject

若您想使用自己搭建的场景,请为这个场景添加预制体 SceneController

为这个场景添加预制体 SceneController

SceneController

SceneController 集成了所有的配置与功能。

场景导入 SceneController 后,可以打开其Inspector面板,请按照您的情况配置。

Main Controller

MainController 是 SceneController上挂载的一个组件

点击 Init Scene,场景中会自动生成所需的GameObject,请在场景中将这些 GameObject 置于所需的位置,随后点击 Assign Transfrom

请设置 MultiviewX_Perception 项目的绝对路径

若使用 LocalFilePedestrianModelProvider, 请设置 Mode_PATH 的绝对路径,此路径务必在一个路径包含”Resources/Models”的文件夹下且此文件夹中有且仅有人物模型的预制体(.prefab),而非.fbx等模型文件。若您的项目中还没有人物模型,可以使用示例模型,其在 com.tsingloo.wildperception-main\Resources\Models 下,请使用其绝对路径。

设置相关路径

对于人物模型,仅仅要求其具有 Humanoid 骨骼,并带有 Animator组件,其 Runtime Animator Controller 可以为空,若为空,将会在其生成时使用您在 People Manager 中配置的默认Runtime Animator Controller。

仅仅要求其具有 Humanoid 骨骼,并带有 Animator 组件

注意:

  1. 请尽量保证GridOrigin_OpenCV的纵坐标(Y)与Center_HumanSpawn_CameraLookAt的纵坐标(Y)相同,否则可能会出现标定不准确的情况,这个问题可能会在后续工作中修复。
  2. 确保场景中供人物模型行走的平面携带有 NavMeshSurface 组件,并已完成烘焙,若您未在 Add Component 中找到这个组件,请前往安装 Package Manager -> Packages: Unity Registry -> AI Navigation,此处使用的版本是 1.1.1

供人物模型行走的平面携带有 NavMeshSurface 组件

Camera Manager

Camera Manager 管理并控制着相机相关的内容,您可以在这里配置将在场景运行多少帧后开始导出序列帧(Begin Frame Count)、相机的位置类型(自动生成 Ellipse_Auto 或者手动摆放 By Hand)、相机的自动生成参数 Ellipse_Auto Settings

Camera Place Type

若您希望程序自动生成相机,请使用Auto

若您希望手动放置相机,请使用 By Hand
并从菜单中添加相机,为这个场景添加相机预制体 Camera_Perception,并将预制体放在HandPlacedCameraParent下

为这个场景添加相机预制体

将相机预制体放在HandPlacedCameraParent下

Ellipse_Auto Settings

此处您可以配置相机的自动生成参数。

参数名 备注
Level 有几层相机
Nums Per Level 每层多少个相机
Height First Level 第一层离地(LookAt) 多少垂直高度(已经换算为了OpenCV下长度)
H Per Level 若有多层,每层层高(已经换算为了OpenCV下长度)
Major Axis 椭圆主轴长度 (已经换算为了OpenCV下长度)
Minor Axis 椭圆副轴长度 (已经换算为了OpenCV下长度)
Camera Prefab 将会被自动生成的相机的预制体

Pedestrians Manager

此处您可以配置人物生成的相关参数。

参数名 备注
Default Animator 人物模型使用的默认动画控制器
Add_human_count 每次敲击空格将会新生成几个人物模型
Preset_humans 场景初始化后生成多少个模型
Largest,Smallest,X,Y 规定初始化生成模型的区域(绿色矩形辅助线)以及行人的活动范围
Outter Bound Radius 人物模型回收边界,请设置大一些,务必使此边界不与 Grid 相交,否则可能出现Editor 卡死,这个问题会在后续工作中修复

AbstractPedestrianModelProvider

实现此类以能为 Pedestrians Manager 提供行人模型(将其拖拽到 Main Controller 的 Pedestrian Model Provider 属性槽中,默认为 LocalFilePedestrianModelProvider)

CalibrateTool

用于相机的标定,为 MultiviewX_Perception 提供数据,请见:CalibrateTool

Notes of the calibration of MultivewX_Perception(CalibrateTool)

WHAT IS NEW!!

支持利用数据集文件夹中 calibrations 中的标定数据与 datasetparameters.py 中的 NUM_CAM MAP_HEIGHT MAP_WIDTH OverlapUnitConvert OverlapGridOffset 参数给出其相机姿态和视野范围。

例如,当我们希望为 Wildtrack 数据集产生 Overlap view 时:

在命令行中输入 -view D:\Wildtrack ,注意需保证其calibrations文件夹下有extrinsicintrinsic文件夹(与Wildtrack格式一致)。

运行可得下列结果。

Camera 6

左图为本工具对 Wildtrack 的结果,右图(有一定变形)为参考

注意:请修改上述的五个参数以符合实际情况,MAP 使用的单位应当与其 calibration 使用的单位相同。Wildtrack 数据集工具声明其网格起点与世界原点并非同处,网格起点为(-300,-90,0)cm,似乎有误。此处使用的是网格起点为(-300,-900,0)cm

Args

MultiviewX_Perception可以接受命令行参数,从而用户可以快速高效地产生数据集。当未接收到相关参数时候,不会启用相关功能。

-a :Annotate and show the bbox on the first frame of each camera or not.

-s :Save the bbox on the first frame of each camera or not.

-k :Keep the remains of Perception dataset or not.

-f :Force calibrate and generate POM, regardless of perception.

-p n: Provide preview for the front n frames. ex. -p 5 will provide 5 frames to preview

-v :Generate Overlap view for the dataset

-view path :Generate Overlap view for the specified dataset, there should be folder calibrations in the given path. ex. -view D:\Wildtrack

例如,当只想借助CalibrateTool进行标定时,可以输入-f,程序会跳过处理percetion数据步骤,也不会进行后续标注的环节。

1
python run_all.py -f

Keep in Mind

CalibrateTool 现在是 WildPerception 的标定部分

Github 项目地址:MultiviewX_WildPerception

欢迎下载示例文件:sample.zip

对于场景:

  1. Unity长度(米)➗ Scaling = OpenCV长度(米)

  2. (Unity点坐标 - Unity中GridOrigin的Unity坐标)➗Scaling , 再交换Unity坐标中的y,z分量,可以得到OpenCV下点坐标。

  3. GridOrigin所在位置是OpenCV下的坐标原点

  4. 棋盘会在黄色辅助正方体内随机生成,辅助正方体的边长等于两倍的tRandomTransform,其中心是chessboardGenerateCenter

  5. 场景中全体markpoint_3dchessboardGenerateCenter的坐标加上一些预设的偏移得到。换言之,场景其实只有一份markpoints_3d,其中心为 chessboardGenerateCenter,均匀分布在水平面上。

    注意:Grid辅助线上标注的和辅助点markpoint_3d上标注的值已经是OpenCV下此点的坐标。

    如下图:

    image-20230322202441746

对于每个相机:

  1. 既然整个场景公用一份markpoints_3d,为什么每个相机下都有markpoints_3d.txt这么个文件呢?

    因为不是全体markpoint_3d都在此相机的视野范围内,需要针对每个相机进行剔除对应点。每个相机的 markpoints_3d.txtmarkpoints_2d.txt 共同得出了其外参。

  2. 务必保证Game View下的分辨率与CalibrateTool配置的分辨率相同,否则会直接退出运行并报错。

  3. tRandomTransform的选择推荐是,使得黄色辅助正方体大部分在所有相机的视野中。

Introduction

CalibrateTool是一个在Unity3D中为一个或多个相机,产生多个虚拟的不同角度朝向的棋盘格数据且给出待标定相机对应内外参的工具。其生成的虚拟棋盘数据等效于利用OpenCV中cv.findChessboardCorners所产生的结果。同时,CalibrateTool 可以完成一些运行 MultiviewX_Perception 所需要的设置,诸如设置地图大小、地图格点起始位置等。具体使用在 [Work with MultiviewX_Perception](# Work with MultiviewX_Perception) 标题下。

下列图片为标注环节的效果演示,此环节不在CalibrateTool能力范围内,是MultviewX_Perception的后续环节。此处贴上标注的图仅仅用来说明CalibrateTool的缩放、OpenCV坐标系的设置、标定是合理有效的。

Grid地图

bbox\_cam9

bbox\_cam8

Setup

CalibrateTool.unitypackage 是对应的Unity资产,其中包含了一个带有CalibrateTool组件的预制体和组件对应的代码。(面板可能因为版本不同略有出入,推荐总是使用最新的一个版本)

  1. unitypackage包导入完成后,我们可以将 CalibrateTool 拖入到需要标定的场景中:

    检视面板

  2. 根据自己项目的情况进行配置,点击加号➕,产生空槽,将场景中需要标定的相机拖入,一个或者多个均可,同时,此字段是公开的,可以利用脚本进行赋值:

    点击➕,拖入待标定相机

    利用脚本进行赋值

  3. 调整相机分辨率,点击Game,选择一个具体的分辨率,此处以1920*1080为例

    选择一个具体的分辨率

  4. 传入一个Transform,chessboardGenerateCenter,用来指示虚拟棋盘的产生位置,同时也规定了标定参照的水平面,这个 Transform 的位置最好能在所需标定的相机的屏幕中央附近(此处为了演示此位置,创建了一个cube,实际使用中只需要创建一个空物体,传入Transform即可,不必考虑其旋转,将被统一清零):

    位置最好能在所需标定的相机的屏幕中央附近

    传入一个Transform

  5. 给定目标文件夹,CalibrateTool会在此文件夹下产生一个 calib 文件夹用来保存数据。一般会填入MultiviewX所在文件夹

    给定目标文件夹

  6. 传入一个Transform,Grid Origin用来指示Grid格点的原点,同时也是OpenCV坐标系(右手坐标系)的原点,为了方便计算,应当将此点设置在标定参照的水平面上(其Unity坐标的Y值应该与chessboardGenerateCenter的Y值相同,我没有测试过不相同会如何)。

    Grid Origin

    正确配置后,Scene场景中会产生辅助线。此图中,蓝色箭头指示右手坐标系下Y轴的正反向,红色箭头指示右手坐标系下X轴的正方向

    指示Grid格点的原点

  7. 可以通过调节MAP_HEIGHTMAP_WIDTH 来调节格点图的大小,MultivewX只会标注脚底在格点图中的人。默认值16 与 25 是一个合理的值,一般不需要额外改动。

    例如,当MAP_HEIGHT = 16 ,MAP_WIDTH = 8 时,标注如图:

    MAP\_WIDTH = 8

    bbox\_cam7

    MAP_EXPAND可以理解成每个边长被额外划分多少份(小刻度),改动此项不会改变地图的大小。

    例如,当MAP_EXPAND = 40 时:

    MAP\_EXPAND = 40

  8. 缩放(Scaling)。很多时候,场景素材的地图尺寸和人物模型尺寸是不一样的,往往会存在人物模型与场景的不协调。 下图展示了这种差异,人物模型看起来很小,成年人看起来身高和儿童一样。

    地图尺寸和人物模型尺寸这就导致用户难以一站式完成CalibrateTool的使用,需要自己手动再去调整人物模型或者场景素材,而往往这种调整还牵扯到matchings(MultiviewX接受的一种输入)的坐标变换,每个人实现matchings的方法都不一样,这里提个醒,坐标变换的顺序必须是: 缩放->旋转->平移

    通过调整Scaling参数,CalibrateTool的辅助线与辅助模型(立方体)可以帮助用户很直观的找到一个合理的缩放值。辅助线的每格的边长为右手坐标系下1米。辅助模型的长宽为MAN_RADIUS*2,高为MAN_HEIGHT

    给与生成辅助线所需引用

    辅助线与模型

  9. 一般情况下,其余参数不需要额外设置。如果Python端报错,可以尝试调大Update Chessboard Interval参数,增加IO读写的时间。

  10. 运行。

Calibrate

拿到数据后,我们就可以进行标定了,应该会有如下结构:calib文件夹下有 C1 - Cn 子文件夹,每个子文件夹中,有得到的棋盘数据。

image-20230301221916504

运行calibrateCameraByChessboard.py,内外参数分别保存在calibration/intrinsiccalibration/extrinsic中。所输出的外参数(对于每一个虚拟的棋盘,都有一个外参负责对应的变换)为对第一个虚拟棋盘的变换,且生成的第一个虚拟棋盘总是与给定的Chessboard Generate Center同面

虚拟棋盘始终与给定的Chessboard Generate Center同面

一个疑惑 这种方法中,Python收到的棋盘格的世界坐标是给定的,如上图所示。(0,0,0)总是在左下角 往往在一些相对Chessboard Generate Center物体对称的物体,会有一个很相近的tvec。 因为在单个相机时,相对于该相机描述时,物体总是需要做同样的位移变换。但是在多个相机时,这种描述,依旧是相对单个接受标定的相机。

为了解决上述的疑问,尝试采用了cv2.solvePnP,和一组在待标定(水)平面上的静态的点来得到R与T,目前不支持相对斜面标定外参。

一组在待标定平面上的静态的点

在引入Grid格点的原点的概念时,遇到了一些问题。原思路是直接变换MarkPoints,使得solvePnP ”认识“目标坐标系,但是,经过实验得知,变换MarkPoints时候,应当保证最终被solvePnP获取的(OpenCV中)世界坐标的x,y值,保证其正方向与原 Unity 中x,z正方向保持一致或全部相反。否则会导致后续POM生成失败。猜测是左右手坐标系变换后OpenCV下Y正方向的问题。

Validate

通过参考Unity3d和OpenCV的相机模型左右手坐标系下三维位姿(旋转、平移)的转换旋转向量和旋转矩阵的互相转换 python cv2.Rodrigues()得知,默认的 Unity3D 相机组件是一个理想的针孔相机,其内外参可以通过调用Unity3D给予的相关参数计算得出。

GetNativeCalibrationByMath(),给出了这种方法,其结果基于Unity场景下的世界坐标系。经过比较这两个方法获得的值,误差很小,可以认为CalibrateTool是可以合理利用的。

提供了 vali.py,其利用单应性检验标定结果。

Preview

cam1\_frames

支持动图预览

  • 例如,在命令行中输入 python run_all.py -p 15 ,即可为前15帧生成带标注框的动图预览。

Work with MultiviewX_Perception

请 clone 一份 MultiviewX_Perception,可以参考Notes of MultiviewX_Perception进行后续工作。其将原calibdateCamera.py替换为了calibrateByChessboard.py,添加了一些动态容量的数组以适应不同的摄像头数,支持Scaling缩放,并且datasetParameters将由 CalibrateTool 根据Unity中Inspector面板处的参数自动生成,等等。

欢迎下载示例文件sample.zip,将其子文件夹calibperceptionmatchings,子文件datasetParameters.py拖入到 MultiviewX_Perception 文件夹下。

黄色字体即拖入的文件

运行run_all.py,参考Notes of MultiviewX_Perception,其给出了一些常用的命令行参数

1
python run_all.py 

当用户仅仅需要标定与生成POM时候(仅仅提供calibdatasetParameters.py),可以输入参数-f:

1
python run_all.py -f 

如果遇到WinError 32 报错,请检查是否有相关程序正在使用.pom,在PyCharm中,可能不小心打开了.pom的预览窗口,请关闭。

image-20230325105913670

示例 datasetParameters.py (for Wildtrack):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
GRID_ORIGIN = [-14.91,-1.51,-5.43]
NUM_CAM = 7
CHESSBOARD_COUNT = 50
MAP_WIDTH = 12
MAP_HEIGHT = 36
MAP_EXPAND = 40
IMAGE_WIDTH = 1280
IMAGE_HEIGHT = 720
MAN_HEIGHT = 1.8
MAN_RADIUS = 0.16
RJUST_WIDTH = 4
Scaling = 1
NUM_FRAMES = 0
DATASET_NAME = ''

# If you are using perception package: this should NOT be 'perception', output path of perception instead
PERCEPTION_PATH = 'D:/Test/WildPerception'

# The following is for -view configure only:

# Define how to convert your unit length to meter, if you are using cm, then 0.01
OverlapUnitConvert = 0.01
# Define how to translate cams to make the world origin and the grid origin is the same
OverlapGridOffset = (3., 9., 0.)

Notes of Unity Perception Package

Setup

Perception 是Unity官方提供的,用以生成计算机视觉相关内容的包。Synthetichumans 是用来快速生成多个人物的资源包。

Perception

通过查阅官方文档完成设置。

  • Click on the plus (+) sign at the top-left corner of the Package Manager window and then choose the option Add package from git URL….
  • 🟢 Action: Enter the address com.unity.perception and click Add.

导入完成,并打开项目后需要更改项目相关设置:

Open Edit -> Project Settings -> Editor, and disable Asynchronous Shader Compilation.

Search for and select the asset named HDRP High Fidelity in your project, and set Lit Shader Mode to Both.

Synthetichumans

通过查阅官方文档完成设置。
因为整个包体积较大,实际采用先Clone后添加的到项目Package列表中的办法。

Add package from disk

在manifest.json中加入以下条目

1
2
"com.unity.cv.synthetichumans": "file:../../com.unity.cv.synthetichumans",
"com.unity.perception": "file:../../com.unity.perception",

Usage

Perception Camera

为相机添加Perception Camera组件,让此相机拥有相关能力。

Camera Labelers & Labeling

对于所需要的ground-truth数据,可以在Camera Labelers中注明。

labeler可以自己创建,Perception 包为我们已经为我们提供了以下常用的labelers:

keypoint labeling, 3D bounding boxes, 2D bounding boxes, object counts, object information (pixel counts and ids), instance segmentation, semantic segmentation, occlusion, depth, normals, and more.

对于一些labelers来说,可以在Unity中实时展示。Show Labeler Visulizations打上勾即可

我们需要告诉Perception Camera应该标记哪些物体。例如,我们想生成一个苹果的ground truth,我们就需要告诉Unity场景中的什么是苹果。场景中的苹果,应该带有”苹果“的标签。
image-20230227201610587 我们可以注意到,Camera Labelers中有ID Label Config字段,这是Perception Camera会去留意的标签的一些配置

创建一个ID Label Config

In the Project tab, right-click the Assets folder, then click Create → Perception → ID Label Config.
image-20230227202819009

我们可以将这个新创建的ID Label Config传给对应的Labeler

对于我们想要被标注的物体, 我们将为其添加Labeling组件。Labeling 表示此物体将携带一些与对应Label Config相联系的标签。可以看到,此处有Use Automatic Labeling字段,勾选时,将用一些规则(通常是此物体所在的文件夹名或资产名),使得此物体自动携带上标签。不勾选时,我们可以手动选择一些建议的标签,或者新建标签。在随后我们可以点击Add to Label Config将此标签添加到对应的Label Config

image-20230227203553862

每个物体都可以携带多个Labels,这样就可以实现不同的物体可以与不同的Camera Labelers交互。例如,我可以将苹果带上水果和苹果的标签,使得一种只关注水果的Camera Labelers获得信息,一种能够希望区分香蕉和苹果Camera Labelers获得相应的信息

我们可以使用Assets → Perception → Create Prefabs from Selected Models来快速导入.fbx的模型文件

Randomizers

随机器generation1

Inspect Synthetic Data

一般的,数据集将以SOLO的格式(数据的保存结构),通过一下方法,我们可以调节相关的设置

Open the Project Settings window, by selecting the menu Edit → Project Settings. Select Perception from the left panel. This will bring up the Perception Settings pane.

Introduction to VR Development

Introduction

Virtual reality (VR) is a simulated experience that employs pose tracking and 3D near-eye displays to give the user an immersive feel of a virtual world.

The equipment I am using now is Pico Neo3 based on PICO Unity Integration SDK

对于使用Unity开发, 其 XR Interactioin Toolkit 包已经提供了相当实用强力的脚本与抽象。本文会给出一些踩的坑和常见用法。

Environment:

  1. Unity Editor 2021.3.5f1c1

  2. XR Interaction Toolkit | XR Interaction Toolkit | 2.2.0

  3. Preview Tool provided by Pico Official

Notice

  • 2.2.0中,XRKT提供了一些新的方法与属性, 一些旧版的被标记为弃用。本例中的代码可能有些无法在老版本中实现,但旧版一般也都提供了近似的方法与属性。

  • 推测在使用Preview Tool时(有线连接),尤其是在PC端提示绿标“连接成功” 时,PC端的以太网会连接到一个”NDIS”设备,导致一些暂时的网络异常(无法访问网页)。

Interaction Toolkit

这可以被理解为使用Unity进行XR开发的核心,进行相关的配置后,几乎一切目前的VR设备可以被抽象为Unity的Device。XR Origin管理着这些设备。

在本文的项目中,直接使用了示例场景中的Introduction to VR Development.prefab预制体,其已经配置好了XR Origin,Input Action Manager InteractionManager EventSystem 等等基础的交互组件。

624b6fa1d9d4a945c593557a04b5a698.png

624b6fa1d9d4a945c593557a04b5a698.png

Interactor & Interactable

XRTK提供的主要交互方式为Interactor,可以大致分为

RayInteractor,Direct Interactor,Teleport Interactor三种,分别可以理解为射线交互,近距离的抓取抛掷交互,传送交互。其均继承自XRBaseControllerInteactor

可以被交互的物体需要挂载Interactable组件,并合理配置其Layer与Interaction Layer

Layer & Raycast Mask

即Inspector中右上角的Layer概念,可以控制对射线的遮罩。

Interaction Layer

配置Interactor可以交互的层,只有Interactable的Interaction Layer被包括在了Interactor的Interaction Layer中,Interactor才能与之交互。

例如:

CUBE 物体挂载的 Interactable组件中,勾选Interaction Layer为Deployable。并且CUBE的Layer 被囊括在Interactor中的Raycast Configuration -> Raycast Mask 中。

对应RayInteractor中的Interaction Layer囊括了此Deployable,那么CUBE可以被此Interactor交互。

e320974c3f47a8dd692c384abe24a8d8.png

e320974c3f47a8dd692c384abe24a8d8.png

498a2344f6b99fd75cf255dc921c6923.png

498a2344f6b99fd75cf255dc921c6923.png

Events

Interactor 对物体的交互可以抽象为两种类型,即Hover(悬停,待选,摸)Select(选取,拿起,抓起)。每个类型又提供了EnteredExited两个事件。

Development Practice

Assign Interaction Layer

通过代码指定Interaction Layer,在以下情况下,分别代表1 - Default 2 - Deployable

d5223dbaf91397bab58910ed077ae74e.png

d5223dbaf91397bab58910ed077ae74e.png

1
2
XRGrabInteractable xRGrabInteractable = gameObject.GetOrAddComponent<XRGrabInteractable>();
xRGrabInteractable.interactionLayers = 2;

Bind Events and Handler Methods

绑定selectEnteredselectExited等等事件,Handler方法接受对应的EventArgs,以XRInteractorHoverEnteredHandler(HoverEnterEventArgs args)为例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class DragManager: SingletonForMonobehaviour<DragManager>
{
//首先声明Interactor类,并在U
public XRDirectInteractor leftDirectInteractor;
public XRRayInteractor leftRayInteractor;

private void InitXRInteractor()
{
leftRayInteractor.hoverEntered.AddListener(XRInteractorHoverEnteredHandler); }
}

private void XRInteractorHoverEnteredHandler(HoverEnterEventArgs args)
{
Debug.log("[Interactor]Enter Hover"):
}

}

获取当前交互的物体

1
2
3
4
5
private void XRInteractorHoverEnteredHandler(HoverEnterEventArgs args)
{
IXRHoverInteractable hoverComponent = args.interactableObject;
GameObject obj = hoverComponent.transform.gameObject;
}

UI Elements

在UI组件上挂载TrackedDeviceGraphicRaycaster,可以使得其接受对应的Hover、Select事件

Input Event

响应对应的按键。

Base Concepts & Models

PICO Neo3为例:

43cc2a7be65a3d7abe62537ceab2e0b1.png

43cc2a7be65a3d7abe62537ceab2e0b1.png

The table below describes the mappings between PICO controller buttons and Unity keys:

Button Unity Keys
Menu CommonUsages.menuButton: represents whether the Menu button has been activated (pressed).
Trigger - CommonUsages.triggerButton: represents whether the Trigger button has been activated (pressed).
- CommonUsages.trigger: represents the degree to which the Trigger button was pressed. For example, in an archery game, it represents how full the bow has been drawn.
Grip - CommonUsages.gripButton: represents whether the Grip button has been activated (pressed).
- CommonUsages.grip: represents the degree to which the Grip button was pressed. For example, in an archery game, it represents how full the bow has been drawn.
Joystick - CommonUsages.primary2DAxisClick: represents whether the Joystick has been activated (pressed).
- CommonUsages.primary2DAxis: represents whether the Joystick has been moved upward, downward, leftward, or rightward.
X/A CommonUsages.primaryButton: represents whether the X/A button has been activated (pressed).
Y/B CommonUsages.secondaryButton: represents whether the Y/B button has been activated (pressed).

Development Practice

下面给出了代码中获取到输入、事件的用法。

也许2.2.0的VRKT提供了相应事件,可以自行搜索用法。此处给出的代码参考PicoXR中的输入事件_窗外听轩雨的博客-CSDN博客 的内容。

Get Input Device

通过XRNode获取设备

1
2
3
4
5
6
//XRNode为枚举变量
//常用的有 Head LeftHand RightHand
//根据这些枚举可以轻松获得指定的头盔,左手柄,右手柄
InputDevice headController = InputDevices.GetDeviceAtXRNode(XRNode.Head);
InputDevice leftHandController = InputDevices.GetDeviceAtXRNode(XRNode.LeftHand);
InputDevice rightHandController = InputDevices.GetDeviceAtXRNode(XRNode.RightHand);

Try Get Input Value

1
2
3
4
5
6
7
8
9
10
InputDevice device;
//省略device的获取 使用前要先获取
public void Test()
{
bool isDown; //记录是否按下
if(device.TryGetFeatureValue(CommonUsages.triggerButton,out isDown) && isDown)
{
//xxxxx 处理逻辑
}
}

InputEvent.cs

将输入转为事件,分离事件与处理逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.XR;
using Common;
/// <summary>
/// 提供各种输入事件
/// </summary>
public class InputEvent:MonoSingleton<InputEvent>
{
//*************输入设别**************************
InputDevice leftHandController;
InputDevice rightHandController;
InputDevice headController;

//**************对外提供公开事件******************
#region public event

public Action onLeftTriggerEnter;
public Action onLeftTriggerDown;
public Action onLeftTriggerUp;

public Action onRightTriggerEnter;
public Action onRightTriggerDown;
public Action onRightTriggerUp;

public Action onLeftGripEnter;
public Action onLeftGripDown;
public Action onLeftGripUp;

public Action onRightGripEnter;
public Action onRightGripDown;
public Action onRightGripUp;

public Action onLeftAppButtonEnter;
public Action onLeftAppButtonDown;
public Action onLeftAppButtonUp;

public Action onRightAppButtonEnter;
public Action onRightAppButtonDown;
public Action onRightAppButtonUp;

public Action onLeftJoyStickEnter;
public Action onLeftJoyStickDown;
public Action onLeftJoyStickUp;

public Action onRightJoyStickEnter;
public Action onRightJoyStickDown;
public Action onRightJoyStickUp;

public Action<Vector2> onLeftJoyStickMove;
public Action<Vector2> onRightJoyStickMove;

public Action onLeftAXButtonEnter;
public Action onLeftAXButtonDown;
public Action onLeftAXButtonUp;

public Action onLeftBYButtonEnter;
public Action onLeftBYButtonDown;
public Action onLeftBYButonUp;

public Action onRightAXButtonEnter;
public Action onRightAXButtonDown;
public Action onRightAXButtonUp;

public Action onRightBYButtonEnter;
public Action onRightBYButtonDown;
public Action onRightBYButtonUp;

#endregion

//提供状态字典独立记录各个feature的状态
Dictionary<string, bool> stateDic;

//单例模式提供的初始化函数
protected override void Init()
{
base.Init();
leftHandController = InputDevices.GetDeviceAtXRNode(XRNode.LeftHand);
rightHandController = InputDevices.GetDeviceAtXRNode(XRNode.RightHand);
headController = InputDevices.GetDeviceAtXRNode(XRNode.Head);
stateDic = new Dictionary<string, bool>();

}
//*******************事件源的触发**************************

/// <summary>
/// 按钮事件源触发模板
/// </summary>
/// <param name="device">设备</param>
/// <param name="usage">功能特征</param>
/// <param name="btnEnter">开始按下按钮事件</param>
/// <param name="btnDown">按下按钮事件</param>
/// <param name="btnUp">抬起按钮事件</param>
private void ButtonDispatchModel(InputDevice device,InputFeatureUsage<bool> usage,Action btnEnter,Action btnDown,Action btnUp)
{
Debug.Log("usage:" + usage.name);
//为首次执行的feature添加bool状态 -- 用以判断Enter和Up状态
string featureKey = device.name + usage.name;
if(!stateDic.ContainsKey(featureKey))
{
stateDic.Add(featureKey, false);
}

bool isDown;
if(device.TryGetFeatureValue(usage,out isDown) && isDown)
{
if(!stateDic[featureKey])
{
stateDic[featureKey] = true;
if(btnEnter != null)
btnEnter();
}
if(btnDown!=null)
btnDown();
}
else
{
if(stateDic[featureKey])
{
if(btnUp!=null)
btnUp();
stateDic[featureKey] = false;
}
}
}

/// <summary>
/// 摇杆事件源触发模板
/// </summary>
/// <param name="device">设备</param>
/// <param name="usage">功能特征</param>
/// <param name="joyStickMove">移动摇杆事件</param>
private void JoyStickDispatchModel(InputDevice device,InputFeatureUsage<Vector2> usage,Action<Vector2> joyStickMove)
{
Vector2 axis;
if (device.TryGetFeatureValue(usage, out axis) && !axis.Equals(Vector2.zero))
{
if(joyStickMove!=null)
joyStickMove(axis);
}
}

//******************每帧轮询监听事件***********************
private void Update()
{
ButtonDispatchModel(leftHandController, CommonUsages.triggerButton, onLeftTriggerEnter, onLeftTriggerDown, onLeftTriggerUp);
ButtonDispatchModel(rightHandController, CommonUsages.triggerButton, onRightTriggerEnter, onRightTriggerDown, onRightTriggerUp);

ButtonDispatchModel(leftHandController, CommonUsages.gripButton, onLeftGripEnter, onLeftGripDown, onLeftGripUp);
ButtonDispatchModel(rightHandController, CommonUsages.gripButton, onRightGripEnter, onRightGripDown, onRightGripUp);

ButtonDispatchModel(leftHandController, CommonUsages.primaryButton, onLeftAXButtonEnter, onLeftAXButtonDown, onLeftAXButtonUp);
ButtonDispatchModel(rightHandController, CommonUsages.primaryButton, onRightAXButtonEnter, onRightAXButtonDown, onRightAXButtonUp);

ButtonDispatchModel(leftHandController, CommonUsages.secondaryButton, onLeftBYButtonEnter, onLeftBYButtonDown, onLeftBYButonUp);
ButtonDispatchModel(rightHandController, CommonUsages.secondaryButton, onRightBYButtonEnter, onRightBYButtonDown, onRightBYButtonUp);

ButtonDispatchModel(leftHandController, CommonUsages.primary2DAxisClick, onLeftJoyStickEnter, onLeftJoyStickDown, onLeftJoyStickUp);
ButtonDispatchModel(rightHandController, CommonUsages.primary2DAxisClick, onRightJoyStickEnter, onRightJoyStickDown, onRightJoyStickUp);

ButtonDispatchModel(leftHandController, CommonUsages.menuButton, onLeftAppButtonEnter, onLeftAppButtonDown, onLeftAppButtonUp);
ButtonDispatchModel(rightHandController, CommonUsages.menuButton, onRightAppButtonEnter, onRightAppButtonDown,onRightAppButtonUp);

JoyStickDispatchModel(leftHandController, CommonUsages.primary2DAxis, onLeftJoyStickMove);
JoyStickDispatchModel(rightHandController, CommonUsages.primary2DAxis, onRightJoyStickMove);
}
}

Bind Events and Handler Methods

将对应的输入事件与处理事件想绑定,此处给出了EnterDownUp三个事件

private void BindXRInputEvent()
{
    InputEvent.Instance.onLeftAXButtonEnter += AXButtonEnterHandler;
    InputEvent.Instance.onLeftAXButtonUp += AXButtonUpHandler;

    InputEvent.Instance.onLeftBYButtonEnter += BYButtonEnterHandler;
    InputEvent.Instance.onLeftBYButonUp += BYButtonUpHandler;
}


private void AXButtonEnterHandler()
{
    Debug.Log("[Input] LeftAXButtonEnter is called");
    if (!hasStarted) return;
    UIManager.Instance.OpenPanel(eUIPanelType.ChooseBluePrintPanel);
}

private void AXButtonUpHandler()
{
    Debug.Log("[Input] LeftAXButtonUp is called");
    if (!hasStarted) return;
    UIManager.Instance.PopPanel();
}

Build

由于Neo3是安卓设备,在Unity打包时会遇到一些打安卓包的坑,主要原因是外网的一些资源不能正常访问。主要的解决办法时换源或者使用代理。

HDRP光照系统讲解

Abstact

场景中的GameObejct挂载Volume组件,生成对应的Profile资源,生成的资源文件会被自动放在Scene路径下与场景名相同的文件夹中。为了使得HDRI Sky和阴影配置真正生效,需要在Light界面中将 Scene Settings Profile 这个Volume资源与之关联。

Volume

Directional Light

平行光灯的Mode为Realtime,所以它提供直接照明。只有光线照到之处才变亮。因为没有间接光照信息。ReflectionHDRP 中 的反射层级先后顺序是:先看场景中有没有Screen Space Reflection(简称SSR,屏幕空间反射),如果没有再找Reflection Probe(反射探针),如果还没有就反射天空。

Ambient Occlusion

通过明暗(光影变换)表示几何细节
c2cf9d78072781eb5db20b4055aa7d61.png
2f6fdb8fd17b1c798280345947236d70.png

环境光遮蔽针对当前镜头中全屏画面进行计算。通过计算场景中的灯光和物体周围环境,让裂缝,孔洞和物体之间交接处等区域的颜色变得更黑。在真实世界中,这些地方通常因为受到周围物体的遮蔽而变得更黑。

Anti-Aliasing

在HDRP中,我们可以使用Camera组件为画面添加抗锯齿效果。FXAA对性能的消耗少画面会变糊,其次是TAA,性能消耗最大的是SMAA(Enhanced Subpixel Morphological Antialiasing)抗锯齿效果最好的是TAA,其次是SMAA(对静态锯齿效果很好),抗锯齿效果最差的是FXAA。

Physical Light Units and Intensities

Visual Environment 组件中选择了HDRI作为天空的类型

Lux光照强度模式,此处的“光强”可以理解为整个场景的明暗,和直接光照导致的高光、暗部是有区别的。

Lux 50000:

9dee749b5fd822c3a38634ea79c95fbb.png

Lux 10000:

54bb782be1a213c6cc7a1488d263113f.png

Lux 1000 + Directional Light

b9bb9307d901fde73d660ac611b36c66.png

Lux 5000 + Directional Light + Indirect Multiplier > 1

6ac525aec2a04ab7e897678176f7415c.png

Lux光照单位表示的光照信息只有方向没有强度衰减信息,也就是说此单位适合用于表示太阳光这一类对于地球来说只有角度变化,没有强度变化的光照信息。

Light Probe

光照探针被认为是一种价廉物美,为动态物体提供间接光照信息和生成动态阴影的高性能解决方案

Bake

确保所有的静态物体被标记为Static

d196d81482cdb93b85c8ea6ef4923e58.png

对于光照烘培来说,只要确保勾选Contribute GI(贡献全局关照)和 Reflection Probe Static 选项即可。

Environment(HDRP)

可以指定所用的环境光Volume,并指定相关联的Sky作为环境光的来源。

Mixed Lighting

要想烘培全局光照,必须在这里勾选Baked Global Illumination(烘培的全局光照) 选项,然后选择光照模式: Baked Indirect 或者 Shadowmask

Baked Indirect:仅仅烘培间接光照信息

Shadowmask

  • 提供最逼真的阴影效果。不仅烘培间接光照信息,也会在Lightmap(光照贴图)中把阴影烘培进去。

  • 适用于远处存在可见物体的场景,比如在高端机器上运行的开放世界游戏。

Lightmapping Settings

Filtering

可以选择一种Denoiser(降噪算法),可以分为Direct Denoiser、Indirect Denoiser和Ambient Occlusion Denoiser选择不同的降噪算法。

一般建议是根据不同的GPU平台为这三项选择同一种降噪算法。

NVIDIA :Optix

AMD:Radeon Pro

Intel:OpenImageDenoise,基于CPU的降噪算法,可适用于所有平台。

Lightmap Padding

控制光照贴图中,按照UV分开的光照贴图区域之间的间隔太小。

如果此间隔太小,有可能造成光照贴图边缘的颜色渗透。

但是如果间隔太大,又会浪费光照贴图空间,增加贴图数量,导致光照贴图在内存中的占用增大。

Indirect Lighting Controller of Volume

间接光照信息被烘培进光照贴图中后,可以使用此Override增强整个场景的间接光照强度。

Indirect Diffuse Intensity 间接光漫反射强度

HDRP将会由烘培获得的光照贴图和光照探针上的数据乘以这个值。‘

如果值为0,那么

IDI = 4 ISI = 1551d8ea8b610f375ac4db13b9ce24126.pngValue = 16 ISI = 1ee8062c2fc3e5aec87042648581efc43.png

Indirect Specular Intensity 间接光高光强度

HDRP会将任何类型(Baked、Custom或者Realtime)的反射探针(Reflection Probe)上的数据乘以这个值。

IDI = 16 ISI = 1(same as above one)ee8062c2fc3e5aec87042648581efc43.png

IDI  =16 ISI = 104316863f946868cdff2b03003ddc6ce9.png

IDI = 0 ISI = 0

该画面中没有任何来自间接光照的漫反射和高光,没有被平行光找到的地方漆黑一片。9199da6a2fe393974a58302059e378af.png

IDI = 0 ISI = 1

该画面中只有来自间接光照的高光,没有漫反射。

d6c0d281c4393b981101c601e0de04c9.png

IDI = 1 ISI = 0

该画面中只有来自间接光照的漫反射,没有高光。

047cd7d712df383557a4a5cde4f694bf.png

IDI = 1 ISI = 1

该画面中只有来自间接光照的漫反射,也有高光。

b454a52aa49bac9b8c93ba787bda6005.png

处理阴影与环境光遮蔽

Directional Light

Volume 框架

Abstract

Volume 框架是HDRP最重要的组成部分之一,其作用如下:

  • 为场景设置来自天空盒的环境光照

  • 设置各种阴影效果,包括场景中的Contact Shadow 和 Micro Shadow

  • 设置场景中的雾效

  • 设置基于屏幕空间的反射(SSRelection)和折射(SSRefraction)、环境光遮蔽(Ambient Occlusion)

  • 设置后处理效果

  • 设置实时光线追踪

在同一个场景中可以添加多个带Volume组件的GameObject。每个Volume的模式可以按照需要设置成Global 或是 Local

Global意味着这个Volumn针对整个场景生效。不管相机朝向哪里,Global模式的Volumn都会被计算在内。

Local模式的Volumn则只能在它的碰撞体作用区域内生效。一般Box Collider即可满足碰撞体需要。

Exposure

曝光指的是相机感光元器件接受的光线强度。曝光的数值越高,相机接受的光线强度越大。

参数说明

Exposure重载包括四种模式:Fixed、Automatic、Curve Mapping 和 Use Physical Camera

Fixed 固定曝光

用于曝光变化不大的场景,也就是场景中的各个区域有类似的曝光强度。

可以使用多个被设置为Local模式的Volume,通过Blend Distance(混合距离)来融合切换不同的Volume的曝光。

由于HDRP的光照系统是完全基于物理的,所以可以参考物理世界真实的曝光值。

5cfd281ebd4bad3543fc5a737f12e351.png

Automatic 自动曝光

根据当前相机的位置信息和相关场景信息来自动调节曝光。非常适用于曝光随位置不同而发生变化的场景,例如从光照强烈的户外进入阴暗的室内或者从山洞中移动到山洞外

可以模拟现实中人眼适应不同曝光的情况。

Limit Min & Limit Max

分别用于控制场景自动曝光的最小值和最大值。可以用这两个值分别调节当相机处于场景的暗部和亮部时整体的曝光。

99b37196985c18f0e7285b3578469b4e.png

当画面靠近场景中的亮部,周围的环境会变暗,但是环境也许不应该“那么”暗。此时可以将Limit Max 调低,以提升整个画面的曝光。f688c683062bbe0c79c0bb2e09b5814d.png

当画面处于场景的暗部,比如在黑暗的角落朝外看,那么外部环境会变亮,可能导致画面亮部曝光过度(月光即使很亮也不应该像白天)此时可以将LImit Min 调高,降低整个画面的曝光。

Curve Mapping 曲线控制曝光

X轴代表目前场景的曝光

Y轴代表我们想要的曝光

专家使用。

Use Physical Camera 物理相机控制曝光

通过物理相机的Iso、Shutter Speed 和 Aperture 参数来控制曝光。

Volume 界面中只剩下Compensation(补偿)这一项。

Metering Mode

用于控制如何采样用于曝光的计算场景中的灯光。

  1. Average 针对整个画面进行采样,结果稳定

  2. Post 针对画面中央区域采样。如果画面中央的采样区域较暗,整个画面可能曝光过度。

  3. Center Weighted

Visual Environment

Intro to HDRP & DXR

Abstarct

从2018版本开始,Unity Editor中共有三套渲染管线。

HD Render Pipeline Asset

HDRP配置文件(HD Render Pipeline Asset)管理HDRP项目的所有渲染功能。HDRP会用这个配置文件生成一个HDRP渲染管线的实例,而这个渲染管线的实例包含用于渲染的中间资源。

为了满足项目开发中不同的需求,HDRP还提供了另外两种渲染方式。

针对不同平台使用不同的HDRP配置文件

在同一个HDRP项目中可以创建多个HDRP配置文件,针对不同的计算平台应用不同的HDRP配置文件。

要针对不同的平台使用不同的设置,需要将对应的HDRP配置文件关联到不同的质量等级上。

质量设置里面的HDRP配置文件会覆盖默认的配置文件。

Frame Settings

帧设置针对的是场景中的Camera、Baked or Custom Reflection 和 Realtime Reflection 的相关设置。后面两个反射相关的设置应用在Reflection Probe上

帧设置的优先级低于HDRP配置文件,也就是说,如果在HDRP配置文件中没有打开某项功能,那么帧设置中对应的功能就会自动被禁用。

帧设置可以让我们为不同的相机和反射探针启用/禁用不同的HDRP功能组合。

Volume 框架

Volume 的作用是通过调整各项HDRP功能的参数,影响相机所看到画面的最终渲染效果。

dd003dc04f47c381deb98503587eb575.png

  • 每个HDRP项目中可以有多个HDRP配置文件

    每个配置文件对应不同的画质或目标平台。但是HDRP项目每次只能使用一个HDRP配置文件,也无法在运行时切换HDRP配置文件。

  • HDRP会为Camera(相机),Backed or Custom Reflection(烘培或自定义反射)和Realtime Reflection(实时反射)提供一套默认的帧设置。如果在HDRP配置中没有被启用,就会在帧设置里被禁用。

  • 注意,在HDRP配置文件中已经启用的功能,也要确保默认帧设置中启用了相关功能。即在HDRP配置文件中启用了某个功能,但是没有在帧设置中启用它,那么在项目中也是无法使用它。

  • 可以为场景中一个(或者多个)相机和反射探针自定义帧设置。 如果在这些自定义帧设置中启用某个功能(前提是在HDRP配置文件中已经启用),那么自定义帧设置中的配置信息会覆盖(Override)默认帧设置中的配置信息。

  • 可以为同一个场景这创建多个Volume。这些Volume的模式可能是全局(Global)的或者是本地(Local)的。如果在当前活跃相机的帧设置(如果没有启用自定义帧设置,就使用默认帧设置)中没有启用某个功能,比如Fog,那么在与此相机相关的Volume中调整Fog参数值就没有意义。

HD Render Pipeline Asset中的7类参数

Rendering

  1. Color Buffer Format

    出于对性能的考虑,HDRP默认使用R11G11B10格式(不包含Alpha通道)

    如果要把HDRP渲染的画面合成到另外的图片上,就需要包含Alpha通道,这时就要选择R16G16B16A16格式。带有Alpha通道的格式会对性能造成一定的影响。

    如果需要将R16G16B16A16格式作为最终渲染输出的格式,那么在Post Processing的Buffer Format中也要选择相同的格式。

  2. Lit Shader Mode

    Lit Shader 是HDRP材质使用的默认着色器。可以选择以下三种模式:

    1. Forward:Lit Shader仅使用前向渲染

    2. Deferred:Lit Shader会使用延迟渲染,一些高级材质还会使用前向渲染。

    3. Both:延迟和前向渲染都可用

      选择Both模式可以通过自定义帧设置为相机选择Deffered或者Forward渲染。不过此模式会让HDRP为两种渲染方式都编译相关的着色器变体。

      如果选择前向渲染或Both模式,则可以选择MSAA

  3. Motion Vector

    HDRP可以在屏幕空间反射和运动模糊中使用运动矢量。通过Camera组件启用的TAA必须使用运动矢量才能工作。如果禁用此选项,则运动模糊和TAA功能将不会正常工作,屏幕空间反射则会用低质量渲染模式。

  4. Runtime Debug Display

    启用该选项后可以在运行时显示灯光和材质的属性信息。正式出包时建议禁用。

  5. Dithering Cross-fade 平滑转换

    这是与Game Object的LOD转换相关的功能。启用该选项后可以让HDRP在做LOD转换时进行平滑的转换。

  6. Terrain Hole 地形洞

    启用该选项后可以显示地形上的凹陷孔洞。如果禁用此选项,则地形上的孔洞不会显示。

  7. 如果你的场景中没有使用透明材质或者没有在Lit材质中使用相关选项,则可以禁用以下选项以减少构建时间。

    1. Transparent Backface 透明背面
    2. Transparent Depth Prepass 透明深度预处理
    3. Transparent Depth Postpass 透明深度后处理

    4e7db5f02099684052b94ad28a7b96ec.png

  8. Custom Pass 自定义通道

    如果没有使用Custom Pass 功能,禁用此功能可以节约内存。

  9. Realtime Raytracing 实时光线追踪

    如果要在HDRP项目中使用实时光线追踪功能,则需要先启用此选项。

  10. LOD Bias LOD 偏差

    场景中的相机会使用此数值来计算LOD偏差。

  11. Maximum LOD Level 最大LOD级别

    用于设置相机支持的最大LOD级别。

  12. Decals 贴花

    1. Draw Distance 用于定义相机离开物体多远以后不再渲染贴花。

    2. Atlas Width和Atlas Height 用于设置纹理图集的宽度和高度。这个纹理图集用于保存场景中所有投射在透明表面上的贴花。

    3. Metal and Ambient Occlusion Properties 启用该选项后,贴花能够影响材质上的金属高光和环境光遮蔽。

    4. Maximum Clustered Decals on Screen 屏幕上能够同时显示的贴花数量(这些贴花影响的是透明表面)。
      e9609527dd29619d4590b419e98c8923.png

  13. Dynamic Resolution 动态分辨率

  14. Low res Transparency 低分辨率透明

    启用该选项后使用低分辨率的透明效果。

Lighting

  1. SSAO(Screen Space Ambient Occlusion)启用此选项后可以为场景添加基于屏幕空间计算的环境光遮蔽效果。可以在Volume中的Ambient Occlusion Override中对效果进行调整。

  2. Volumetrics 体积光

    为场景中的灯光和雾效增加体积光效果。

  3. Light Layers 光照层可以

    在这里启用/禁用光照层(Light Layers)功能。此功能可以让场景中的光源只照亮指定的物体,忽略无关的物体。在HDRP Global Settings -> Layers Names 中可以见到对应层

  4. Cookies 光线遮罩

  5. Reflections 反射
    9494534745278471e7e3a3217ea91803.png

  6. Sky 天空
    2524c9d6c97cc60d8bb2ac333a2fe060.png

    1. Reflection 当场景中没有如何反射探针可以用于计算物体表面的反射信息时,HDRP会使用天空盒Cubemap来计算反射信息。Reflection Size可以控制用于计算反射信息的天空盒Cubemap的分辨率。此分辨率斌不会影响天空盒本身的质量。

    2. Lighting Override Mask 可以让环境光照和天空背景进行分离。如果在此指定了一个Layer而不是使用默认的Nothing,那么HDRP会在场景中寻找与此Layer相关联的Game Object,如果找到的GameObject中包含Volume组件而且可以对当前相机产生影响,那么HDRP就会使用这些Volumn中的信息来计算环境光照。

  7. Shadow 阴影
    d1b2413c2a758ae18a76e6a00215c0a2.png

    1. Shadowmask

      控制Shadowmask光照模式(Shadowmask Lighting Mode)的启用/禁用。

    2. Maximum Shadows on Screen 同屏显示最大阴影数量

      超过这里设定的阴影数量之外的阴影将不被渲染。

    3. Filtering Quality 过滤质量

      选择高质量可以提升阴影质量,减少阴影边缘的锯齿。在Forward模式和Both模式下,可以选择Low,Medium和High三档质量。在Deferred模式下,只能使用Medium质量。

    4. Screen Space Shadows

      启用该选项后,HDRP会在一个单独的通道中计算基于屏幕空间的阴影。

    5. Use Contact Shadows 使用接触阴影

      此处可以选择Low、Medium、High质量。然后在Light组件中可以选择可用的接触阴影质量。

      如果在HDRP配置文件中不勾选上述任何一项,那么只能在Light组件中选择Custom选项。

      要使用接触阴影,需要在Default Frame Settings中启用Contact Shadows选项。

Lighting Quality Settings

光照质量的相关设置,配合Lighting中的参数使用,调整相应数值以达到质量与性能的平衡。

Material

  1. Available Material Quality Levels 可用材质质量等级

    默认所有材质质量都可以使用

  2. Default Material Quality Level 默认材质质量等级

  3. Distortion 变形

  4. Subsurface Scattering 次表面散射

    可以很好的表示光在材质内多次反弹折射等等效果,玉石、翡翠等应用此效果明显。

  5. Fabric BSDF Convolution                             

    若启用此选项,则在使用Fabric材质时,HDRP会单独为织物着色器(Fabric Shader)计算一份反射探针数据,用于生成更准确的光照效果。不过这样做会导致项目中存在两份光照数据,也会导致目前可见的反射探针数量减少一半。

  6. Diffusion Profile List 漫射配置文件列表

    在此保存用于控制次表面散射效果和半透明效果的Diffusion Profile。一个HDRP配置文件最多可以保存15个Diffusion Profile。

Post-processing

  1. Grading LUT Size & Grading LUT Format

    Size用于控制颜色分级时所用的LUT(Lookup Texture)的大小。默认数值32提供了比较平衡的速度和质量表现。

    Format用于设置LUT的编码格式。可以选择R11G11B10R16G16B16A16或者R32G32B32A32格式。精度越高的格式颜色越精准。

Post-processing Quality Settings 后处理质量设置

与光照质量设置类似,可以为后处理效果设置采样值。

e21c555d3500b554ee4bec37f8590f04.png

目前可以设置Depth of Field、Motion Blur、Bloom、Chromatic Aberration(色差)

XR设置

HDRP在特定的平台上输出VR应用

PC需要DX11支持

Ray Tracing

Unity 支持的光追是基于HDRP的,一个光追的Unity项目,首先是一个HDRP项目

利用await/async 与 Task 关键词替换协程功能

Intro

参考视频:# Unity async / await: Coroutine’s Hot Sister [C# & Unity]

协程是Unity提供的异步解决方法,但是实际应用时有诸多不便与不优雅之处。本文将用几个例子对比参照协程与await/async关键字。当然,Unity指出,在Unity中写多线程代码应当参考Unity - Manual: C# Job System

旧例

利用协程转动三个物体,并使得这三个物体逐个停止。

给出下列协程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
using System.Collections;
using UnityEngine;

public class Shape: MonoBehaviour()
{
public IEnumerator RotateForSeconds(float duration)
    {
    var end = Time.time + duration;
    while(Time.time < end)
    {
    transform.Rotate(new Vector3(1,1)*Time.deltaTime*150;
    yield return null;
    }
    }
}

给出下列调用协程的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
using System.Collections;
using UnityEngine;

public class ShapeManager: MonoBehaviour()
{
[SerializeField] private Shape[] _shapes;

public void BeginTest()
{
for(var i = 0; i<_shapes.Length;i++)
{
StartCoroutine(_shapes[i].RotateForSeconds(1+1*i);
}
}
}