项目地址:
Li-Kira/Computer-Vision-based-Sentiment-Analysis-and-Generation-Experience-in-Unity

Unity相关

Asset

项目结果如表所示:

Asset Type Explanation
Fonts This folder contains the fonts used in the project. 此文件夹包含项目中使用的字体。
Materials These assets contain the materials used in the development process.这些资产包含了开发过程中使用的材质。
MLModels The ONNX model used for machine learning.机器学习所用到的ONNX模型。
Prefabs These are reusable GameObjects with prebuilt Components.Add them to a scene to build.这些是预制件游戏对象,可以直接将它们添加到要构建的场景中。
Scripts All user-developed code for gameplay appears here. 所有整理后的项目代码都在这里。
Settings These assets store render pipeline settings,such as UniversalRender Pipeline (URP).这些资产存储渲染管道设置:UniversalRender管道(URP)。
Shaders These programs run on the GPU as part of thegraphics pipeline. 这些Shader作为图形管道的一部分在GPU上运行。
Scenes Runnable Unity scenarios that store test cases generated during development and final integration scenarios. 可运行的Unity场景,存储开发中产生的测试案例以及最后的整合场景。
Textures lmage files can consist of texture files for materials andsurfacing, UI overlay elements for user interface, andlightmaps to store lighting information.图像文件:由材质和表面处理的纹理文件、用户界面的UI覆盖元素以及用于存储照明信息的光图组成。
ThirdParty Assets from external sources, development plug-ins, etc. 来自外部的资产、开发包等。

Requirements

Unity URP兼容版本,本项目使用的版本为:12.1.7
在顶部导航栏中,选择 Window > Package Manager 以打开 Package Manager 窗口。选择 All 选项卡。此选项卡显示当前运行的 Unity 版本的可用资源包列表。从包列表中选择 Universal RP。在 Package Manager 窗口的右下角,选择 Install。Unity 会将 URP 直接安装到您的项目中。

Compatibility (兼容性)

Package version Minimum Unity version Maximum Unity version
12.0.0 2021.2 2021.2
11.0.0 2021.1 2021.1

Graphics Settings (图形设置)

使用线性色彩空间比使用伽马色彩空间提供更准确的渲染,但是部分VR设备如PICO打包仅支持伽马色彩空间,因此打包到PICO需要更改色彩空间。
Player 设置(菜单:Edit > Project Settings__,然后选择 Player__ 类别)中将 Color Space 设置为 Linear

Package Used(包)

以下为项目使用的重要包:

  • Cinemachine Version 2.8.9: Smart camera tools for passionate creators. 智能相机工具。
  • Input System Version 1.4.2: A new input system which can be used as a more extensible and customizable alternative to Unity’s classic input system in UnityEngine.Input.一个新的输入系统,可以作为UnityEngine.Input中Unity经典输入系统的一个更可扩展和可定制的替代。
  • Universal RP Version 12.1.7: The Universal Render Pipeline (URP) is a prebuilt Scriptable Render Pipeline, made by Unity. URP provides artist-friendly workflows that let you quickly and easily create optimized graphics across a range of platforms, from mobile to high-end consoles and PCs.通用渲染管线(URP)是一个预先构建的可编写脚本的渲染管线,由Unity制作。URP提供了艺术家友好的工作流程,让你在一系列的平台上,从手机到高端游戏机和PC,快速而轻松地创建优化图形。
  • Barracuda Version 3.0.0: Barracuda is lightweight and cross-platform Neural Net inference library. Barracuda supports inference both on GPU and CPU. Barracuda是轻量级和跨平台的神经网络推理库。Barracuda支持在GPU和CPU上进行推理。
  • OpenCVForUnity Version 2.4.9TrialVersion: OpenCV for Unity is a resource plugin that allows the use of OpenCV 4.4.0 in Unity. Unity 的 OpenCV 是一个资源插件,可在 Unity 中使用 OpenCV 4.4.0。
  • PICO Unity Integration SDK Version 2.0.7: Unity XR SDK v2.x是LTS版本,目前支持Neo 3和PICO4系列。
  • Shader Graph Version 12.1.7: The Shader Graph package adds a visual Shader editing tool to Unity. You can use this tool to create Shaders in a visual way instead of writing code. Specific render pipelines can implement specific graph features. Currently, both the High Definition Rendering Pipeline and the Universal Rendering Pipeline support Shader Graph. Shader Graph软件包为Unity添加了一个可视化的Shader编辑工具。你可以使用这个工具以可视化的方式创建着色器,而不是写代码。特定的渲染管线可以实现特定的图形功能。目前,高清渲染管线和通用渲染管线都支持Shader Graph。

外部服务:

  • NeteaseCloudMusicApi: NeteaseCloudMusic Node.js API service. 网易云Node.js API服务。

Environment Settings

Model (模型)

房间模型来源于Unity Asset Store免费素材:

https://assetstore.unity.com/packages/3d/props/furniture/pack-gesta-furniture-1-28237

Material (材质)

使用URP渲染管线,需要将材质从Build-in转换成URP兼容的材质,否则材质贴图会出现洋红色的显示错误
选择 Window > Rendering > Render Pipeline Converter。Unity 将打开 Render Pipeline Converter 窗口,可以在此窗口内将我们的材质转换为URP材质。

Lighting (光照)

在设置自己的光照之前需要清除原来的光照探针,并生成新的光照设置
Mesh (网格)

对于每一个模型文件的导入,需要在MeshImport Setting里面勾选Generate Lightmap UVs,如图所示:Generate Lightmap UVs.png

同时需要设置Static Gameobject,由于静态游戏对象不会移动,因此这些计算的结果在运行时仍然有效。这意味着Unity可以节省运行时计算成本,并可能提高性能,对于每一个Mesh的设置如图:Mesh Settings.png
Light Mode (光照模式)

在Lighting>Mixed Lighting 里面,将光照模式改为Backed Indirect
Unity 在 Unity Editor 中为烘焙光源执行计算,并将结果作为光照数据保存到磁盘中。这一过程称为烘焙。在运行时,Unity 将加载烘焙的光照数据,并使用这一数据来照亮场景。由于复杂的计算是预先执行的,因此烘培光源可以减少运行时的着色成本,并减少阴影的渲染成本。

Sky Box (天空盒)

Skybox设置如图:Skybox.png
将HDR贴图在Import Setting里面转换为Cubemap,并以此生成材质

Reflection Probe (反射探针)

使用前请先清除原先的反射探针,并将新的反射探针的大小调整为能够覆盖整个房间的大小
Light source (光源)

使用的光源在层级视图显示如下:Light source.png,光源的详细设置如图:Light source Settings.png
其中,Area Light模拟环境光,Directional Light模拟太阳光

Lighting Settings (光照设置)

光源设置如图所示: Lighting Settings.png,烘焙后生成的光照设置文件如图:Lighting Settings File.png
同时,在URP Settings中将Shadow Resolution调为4096,并且启用Soft Shadows,这可以增加场景阴影的细腻度

Post Process (后处理)

使用前请在Camera>Rendering中启用Post Process,Post Process设置如图:Post Process.png
其中Tonemapping(色调映射)是将颜色值从高动态范围(HDR)映射到低动态范围(LDR)的过程。在Unity中,对于大多数平台,任意的16位浮点颜色值将被映射为传统的8位值。

Bloom给人一种极其明亮的光线的错觉,让明亮的像素扩散到暗部去,以达到增强场景视觉氛围的效果。

Lift Gamma Gain这种效果可以让你进行三段式调色。Lift Gamma Gain轨迹球遵循ASC CDL标准。当你调整轨迹球上的点的位置时,它将图像的色调向给定色调范围内的那个颜色转移。使用不同的轨迹球来影响图像中的不同范围。调整轨迹球下面的滑块,以抵消该范围的颜色明度。

White Balance消除了不真实的色差。


Media Player

Scripts Info

Script Name Type Description
MediaPlayerController MonoBehaviour 媒体播放器控制脚本
Track ScriptableObject 将歌曲转化为Unity可识别的AudioClip对象脚本

部分参数/函数信息:

Parameters/Function 参数/函数 Type 类型 Return 返回值 Description 描述
trackSources Track[] trackAudioClip 存储AudioClip对象的数组
trackTextUI Text trackSources[trackIndex].name 当前播放音轨名称
trackIndex int cam.ScreenPointToRay(position); 当前播放音轨序列
MediaAudioSource AudioSource trackSources[trackIndex].trackAudioClip AudioClip对象
updateTrack(int index) void void 更新音轨
PlayAudio() void void 播放音轨
PauseAudio() void void 暂停音轨
StopAudio() void void 停止音轨
ForwardButton() void void 向前音轨
BackForwardButton() void void 向后音轨

Painting System

主流的实现方法有以下三种,通过调研选择在该项目中使用第三种实现方式即在UV空间绘图:

  • 使用顶点绘图
  • 使用光照贴图绘图
  • 在UV空间绘图

Prefabs Info

Prefabs 预制件 Parameters 参数 Description 描述
PaintManager 如图PaintManager.png 读取Shader,保存paintable对象的UV信息等
MousePainter 如图MousePainter.png 判断鼠标是否点击,是则执行paint方法

Scripts Info

Shaders Scripts
ExtendIslands PaintManager
TexturePainter Paintable
Paintable MousePainter

Painting Shader

为了实现UV空间绘图,我们使用了三个Shader,他们的作用如下:

  • ExtendIslands: 转换UV的空间坐标。
  • TexturePainter: 画笔的mask信息以及笔刷颜色、大小等信息。
  • Paintable: 渲染遮罩的样式。

Vertex Shader

在顶点着色器中,rasterizer光栅将重新创建UV,而不是将UV空间坐标转换成屏幕空间坐标
同时,由于rasterizer的参数约束在(-1,1)之间,我们需要重新映射UV,这里使用了Unity提供的宏: _ProjectionParams

1
2
3
4
5
6
7
8
9
>v2f vert (appdata v){
            v2f o;
o.worldPos = mul(unity_ObjectToWorld, v.vertex);
            o.uv = v.uv;
float4 uv = float4(0, 0, 0, 1);
            uv.xy = float2(1, _ProjectionParams.x) * (v.uv.xy * float2( 2, 2) - float2(1, 1));
o.vertex = uv;
            return o;
        }
1
2
3
4
5
6
7
8
#### Fragment Shader

>**在片元着色器中,我们使用mask函数来追踪画笔的世界坐标与每个片元之间的距离**
```plain
float mask(float3 position, float3 center, float radius, float hardness){
              float m = distance(center, position);
              return 1 - smoothstep(radius * hardness, radius, m);    
          }

同时,在着色器中加入画笔的半径、硬度和强度的参数
为了支持多种颜色的混合,对背景颜色和画笔颜色进行插值

1
2
3
4
5
6
7
8
9
10
fixed4 frag (v2f i) : SV_Target{   
              if(_PrepareUV > 0 ){
                  return float4(0, 0, 1, 1);
              }        

              float4 col = tex2D(_MainTex, i.uv);
              float f = mask(i.worldPos, _PainterPosition, _Radius, _Hardness);
              float edge = f * _Strength;
              return lerp(col, _PainterColor, edge);
          }

Mouse Painter Scripts

为了能让鼠标进行绘画,我们使用了以下三个脚本:

  • PaintManager:ExtendIslands ShaderTexturePainter Shader中分别读取UV信息以及画笔的颜色大小硬度强度信息
  • Paintable: 挂载在可以被Paint的对象上,并将该对象的信息保存到渲染缓冲区中。
  • MousePainter: 继承自PaintManager,可以通过鼠标点击调用Paint方法。
    PaintManager

从ExtendIslands Shader和TexturePainter Shader中分别读取UV信息以及画笔的颜色、大小、硬度和强度信息
使用CommandBuffer API获取渲染命令列表,当我们需要执行Paint操作时,从渲染缓冲区中执行
使用Singleton,通过这种抽象,让我们想要的Painter继承PaintManager,使其能调用其中的Paint方法。比如说目前项目用到的MousePainter,未来嵌入VR可以针对手柄的Input设计我们想要的Painter
部分参数/函数信息:

Parameters/Function 参数/函数 Type 类型 Return 返回值 Description 描述
prepareUVID int Shader.PropertyToID(“_PrepareUV”); 从Shader中提供的物体UV的ID
positionID int Shader.PropertyToID(“_PainterPosition”); 从Shader中提供的画笔位置的ID
hardnessID int Shader.PropertyToID(“_Hardness”); 从Shader中提供的画笔硬度的ID
strengthID int PropertyToID(“_Strength”); 从Shader中提供的强度位置的ID
radiusID int Shader.PropertyToID(“_Radius”); 从Shader中提供的画笔大小的ID
colorID int Shader.PropertyToID(“_PainterColor”); 从Shader中提供的画笔颜色的ID
textureID int Shader.PropertyToID(“_MainTex”); 从Shader中提供的画笔遮罩的ID
uvOffsetID int Shader.PropertyToID(“_OffsetUV”); 从Shader中提供的变换后UV的ID
uvIslandsID int Shader.PropertyToID(“_UVIslands”); 从Shader中提供的变换后UV的ID
command CommandBuffer command.SetRenderTarget(mask); 渲染指令
paint() public void 实现绘画的方法

Paintable

挂载在能够被绘画的网格模型上,存储模型信息给paint方法使用
部分参数/函数信息:

Parameters/Function 参数/函数 Type 类型 Return 返回值 Description 描述
getUVIslands() RenderTexture uvIslandsRenderTexture UV坐标信息
getMask() RenderTexture maskRenderTexture 遮罩信息
maskTextureID int Shader.PropertyToID(“_MaskTexture”); _MaskTexture

MousePainter

实现鼠标点击流程:通过相机获取反射射线来判断是否击中,是则执行paint方法操作
部分参数/函数信息:

Parameters/Function 参数/函数 Type 类型 Return 返回值 Description 描述
cam Camera Unity相机
click bool true/false 是否点击
ray Ray cam.ScreenPointToRay(position); 从相机返回的射线
p Paintable hit.collider.GetComponent() 执行paint方法

部分执行脚本片段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
if (click){
  Vector3 position = Input.mousePosition;
  Ray ray = cam.ScreenPointToRay(position);
  RaycastHit hit;

  if (Physics.Raycast(ray, out hit, 100.0f)){
      Debug.DrawRay(ray.origin, hit.point - ray.origin, Color.red);
      transform.position = hit.point;
      Paintable p = hit.collider.GetComponent<Paintable>();
      if(p != null){
          PaintManager.instance.paint(p, hit.point, radius, hardness, strength, paintColor);
      }
  }
}

Shader Graph

该Shader用于渲染遮罩,实现方式如下:
使用Default-Particle作为遮罩,在其边缘增加噪声,如图Mask.png所示
通过插值混合网格和遮罩,如图Lerp.png所示


Interaction & Camera Switcher

Scripts Info

Script Name Type Description
IInteractable interface 互动接口
InteractionManager MonoBehaviour 管理互动脚本,包括互动以及相机切换
CanvasInteraction MonoBehaviour, IInteractable 实现IInteractable接口,定义了画布互动方法
BrushesInteraction MonoBehaviour, IInteractable 实现IInteractable接口,定义了笔刷互动方法
UIRotationFixed MonoBehaviour 修正UI显示位置

InteractionManager

管理互动脚本
部分参数/函数信息:

Parameters/Function 参数/函数 Type 类型 Return 返回值 Description 描述
PlayerCamera CinemachineVirtualCamera CinemachineVirtualCamera 角色跟随虚拟相机
CanvasCamera CinemachineVirtualCamera CinemachineVirtualCamera 画布相机
MainCamera Camera Camera 主相机
interactionDistance float 2f 交互距离
interactionUI GameObject UI Mono 交互UI
interactionText TextMeshProUGUI interactable.GetDescription() 交互文本
canvasUI GameObject UI Mono 画布UI
isEButtonHit bool false/true 判断是否按下按键
OnEnable() void void Register CinemachineVirtualCamera
OnDisable() void void Unregister CinemachineVirtualCamera
LockMouse() void void 锁定鼠标
InteractionRay() void void 判断是否被准星选中,选中则显示UI,选择如果按下对应按键则触发互动以及切换相机

主要执行片段:

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
void InteractionRay()
{
  Ray ray = MainCamera.ViewportPointToRay(Vector3.one / 2f);
  RaycastHit hit;

  bool hitSomething = false;

  //是否被准星选中,选中则显示
  if (Physics.Raycast(ray, out hit, interactionDistance))
  {
      IInteractable interactable = hit.collider.GetComponent<IInteractable>();
      if (interactable != null)
      {
          hitSomething = true;
          interactionText.text = interactable.GetDescription();

          //查看交互对象时按下E键进行交互:转换镜头以及调用Interact()方法
          if (Input.GetKeyDown(KeyCode.E))
          {                  
              if (CameraSwitcher.IsActiveCamera(PlayerCamera))
              {
                  CameraSwitcher.SwitchCamera(CanvasCamera);
                  // 解锁鼠标
                  UnlockMouse();
                   
                  //解锁UI
                  isEButtonHit = true;
              }

              if (CameraSwitcher.IsActiveCamera(CanvasCamera))
              {
                  interactable.Interact();
              }

               
          }
      }
  }

  if (Input.GetKeyDown(KeyCode.Escape))
  {
      if (CameraSwitcher.IsActiveCamera(CanvasCamera))
      {
          CameraSwitcher.SwitchCamera(PlayerCamera);
          // 锁定鼠标
          LockMouse();
          // 锁定UI
          isEButtonHit = false;
      }
  }

CanvasInteraction

部分参数/函数信息:

Parameters/Function 参数/函数 Type 类型 Return 返回值 Description 描述
GetDescription() string “Press E to Paint” 准星触发UI显示的文本
Interact() void void 交互行为


Web Request

Scripts Info

Script Name Description
ApiHandler Api调用
NetEasyCloudMusic_HttpClient 封装从网络API中获取JSON的过程
PlayListData 将JSON反序列化为.Net的PlayListData对象
SongInfoData 将JSON反序列化为.Net的SongInfoData对象

Dependency

网易云音乐 Node.js API service
https://github.com/Binaryify/NeteaseCloudMusicApi

NodeJS 8.12+

Install

1
2
3
$ git clone git@github.com:Binaryify/NeteaseCloudMusicApi.git
$ cd NeteaseCloudMusicApi
$ npm install

Run

1
$ node app.js

服务器启动默认端口为 3000,若不想使用 3000 端口,可使用以下命令: Mac/Linux

1
$ PORT=4000 node app.js

windows 下使用 git-bash 或者 cmder 等终端执行以下命令:

1
$ set PORT=4000 && node app.js

ApiHandler

调用Api,并解析返回的Json
部分参数/函数信息:

Parameters/Function 参数/函数 Type 类型 Return 返回值 Description 描述
text TextMeshProUGUI 准星触发UI显示的文本
Keywords string void 歌曲关键词
id string inputField.text
url string http://localhost:3000/playlist/track/all?id=“+ id + “&limit=10&offset=1”
inputField TMP_InputField TMP_InputField
GetPlayList() public async void void
GetSong() public async void void

调用API并获取Json内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var httpClient = new NetEasyCloudMusic_HttpClient();
var result = await httpClient.Get<PlayListData>(url);
text.text = "";


foreach (Playlist VARIABLE in result.result.playlists)
{
  text.text +=
      "id: " + VARIABLE.id + "\n" +
      "name: " + VARIABLE.name + "\n" +
      "trackCount" + VARIABLE.trackCount + "\n"
      + "\n";

}
变量名 必填 类型 实例值 描述
keywords string 满意 关键词
type int 1000 搜索类型:歌单
limit int 10 偏移值
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
{
  "result": {
      "searchQcReminder": null,
      "playlists": [
          {
              "id": 2468397152,
              "name": "【纯音乐】闹钟满意选择",
              "coverImgUrl": "https://p1.music.126.net/7BPYzlYTQp285WXdO-u2Ug==/18215609137826245.jpg",
              "creator": {
                  "nickname": "克里斯不是托",
                  "userId": 1546444700,
                  "userType": 0,
                  "avatarUrl": null,
                  "authStatus": 0,
                  "expertTags": null,
                  "experts": null
              },
              "subscribed": false,
              "trackCount": 30,
              "userId": 1546444700,
              "playCount": 1565,
              "bookCount": 8,
              "specialType": 0,
              "officialTags": null,
              "action": null,
              "actionType": null,
              "recommendText": null,
              "score": null,
              "description": "",
              "highQuality": false
          }
      ],
      "playlistCount": 172
  },
  "code": 200
}

NetEasyCloudMusic_HttpClient

封装从网络API中获取JSON的过程

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
public async Task<TResultType> Get<TResultType>(string url)
{
   
  using var webRequest = UnityWebRequest.Get(url);
   
  webRequest.SetRequestHeader("Content-Type","application/json");

  var operation = webRequest.SendWebRequest();

  while (!operation.isDone)
  {
      await Task.Yield();
  }

  var jsonResponse = webRequest.downloadHandler.text;
   
  if (webRequest.result != UnityWebRequest.Result.Success)
      Debug.LogError($"Failed: {webRequest.error}");


  try
  {
      var result = JsonConvert.DeserializeObject<TResultType>(jsonResponse);
      Debug.Log($"Success:{webRequest.downloadHandler.text}");
      return result;
  }
  catch (Exception exception)
  {
      Debug.LogError($"Could not parse response {this}{jsonResponse}. {exception.Message}");
      return default;
  }
}

JsonDataClass

将JSON反序列化为.Net对象

Name 对象名 Content 内容 Description 描述
PlayListData PlayListResult Playlist 搜索相关歌单信息
SongInfoData Song 歌单内歌曲信息

PlayListData

Parameters/Function 参数/函数 Type 类型 Return 返回值 Description 描述
Playlist class id name trackCount 歌单歌曲部分信息
PlayListResult class List playlists 搜索歌单结果
PlayListData class public List songs { get; set; } root

SongInfoData

Parameters/Function 参数/函数 Type 类型 Return 返回值 Description 描述
Song class public string name { get; set; } public int id { get; set; } song content
SongInfoData class public List songs { get; set; } root