WebGL整合到angular项目中

背景

工作中用到unity来制作实物展示等功能,
其中unity可以导出一些使用了WebGL的html和js文件,
而这个需要和目前的angular项目结合起来.
但由于unity官方的模板太过非专业,代码不利于移植,
只能选择将其特性在angular中重现.

前提知识

  • angular的组件和路由的基本写法

Webgl导出结构

1
2
3
4
5
6
7
8
9
10
11
12
13
├── Build
│ ├── output.data.unityweb(项目资源等)
│ ├── output.json(项目入口文件)
│ ├── output.wasm.code.unityweb
│ ├── output.wasm.framework.unityweb
│ └── UnityLoader.js
├── index.html(用于web服务器的首页,可被替代)
└── TemplateData(项目中的一些图片等资源)
├── favicon.ico
├── ...
├── style.css(加载前后使用的css)
├── UnityProgress.js(进度条显示工具)
└── webgl-logo.png

其中 Build 文件夹下所有文件必不可少, TemplateData 文件夹下资源可以共用
index.html 是主要的被替代的文件.

unity官方太过非专业,导出的内容都一再变化.这里是2019版导出的内容
2020版的内容中,众多文件名发生了变化,同时也去掉了 output.jsonUnityProgress.js 文件

整合到angular后结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
src
├── app
│ ├── xxx-routing.module.tx
│ ├── components
│ │ ├── unity
│ │ │ ├── index.ts
│ │ │ ├── unity.component.css
│ │ │ ├── unity.component.html
│ │ │ ├── unity.component.spec.ts
│ │ │ └── unity.component.ts
.
.
.
├── assets
│ ├── Build
│ └── TemplateData
.
.
.

可以将 BuildTemplateDate 放在 src/assets 下.
然后以组件的方式添加入口.

整合方针

  1. 将集中在一个html文件中的代码分离到angular的各个文件中
  2. 使用angular的风格,重写进度条的数值更新与显示与否的逻辑.

2019版本整合前后

整合前

index.html

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
<!DOCTYPE html>
<html lang="en-us">
<head>
<meta charset="utf-8">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Unity WebGL Player | Heart45</title>
<link rel="shortcut icon" href="TemplateData/favicon.ico">
<link rel="stylesheet" href="TemplateData/style.css">
<script src="TemplateData/UnityProgress.js"></script>
<script src="Build/UnityLoader.js"></script>
<script>
var unityInstance = UnityLoader.instantiate("unityContainer", "Build/output.json", {onProgress: UnityProgress});
</script>
</head>
<body>
<div class="webgl-content">
<div id="unityContainer" style="width: 960px; height: 600px"></div>
<div class="footer">
<div class="webgl-logo"></div>
<div class="fullscreen" onclick="unityInstance.SetFullscreen(1)"></div>
<div class="title">Heart45</div>
</div>
</div>
</body>
</html>

外部文件说明:

  1. style.css 主要用于管理背景图等等
  2. UnityProgress.js 主要用于动态修改进度条

整合后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<ng-container>
<div class="webgl-content">
<!-- 与selector名称对应 -->
<div #unityEl id="unityContainer" style="width: 960px; height: 600px"></div>

<div *ngIf="!isReady"> <!-- 使用angular来管理进度条的显示 -->
<div class="logo Dark"></div>
<div class="progress Dark" style="width: 80%;">
<p style="color: white;">loading......</p>
<div class="full" [style.width.%]="progress*90"></div> <!-- 使用angular变量来动态改变进度条的长度 -->
<div class="empty" [style.width.%]="(1-progress)*90"></div>
<div class="number" style="color: white;">{{(progress * 100).toFixed(1)}}%</div>
</div>
</div>

<div class="footer">
<div class="webgl-logo"></div>
<div class="fullscreen" (click)="toggleFullScreen()"></div> <!-- 将按下后的函数移动到ts中 -->
<div class="title">截面模型</div>
</div>
</div>
</ng-container>

组件类要管理好各个状态的更新,比如progress,isReady等

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
import { Component, OnInit, Input, OnDestroy } from '@angular/core';

@Component({
selector: 'unity',
templateUrl: './unity.component.html',
styleUrls: ['./unity.component.css']
})
export class UnityComponent implements OnInit, OnDestroy {

gameInstance: any;
progress = 0;
isReady = false;

constructor() { }

ngOnInit(): void {
// 此处的UnityLoader外部库,要在angular中配置,以达成对UnityLoader.js外部js文件的引用
const loader = (window as any).UnityLoader;

this.gameInstance = loader.instantiate('gameContainer', `/assets/Build/output.json`, { // 移动js部分到ts中
onProgress: (gameInstance: any, progress: number) => {
this.progress = progress; // 由于angular有双向绑定,可以动态修改变量值
if (progress === 1) {
this.isReady = true;
}
}
});
}

/**
* 看起来是生命周期相关的函数
*/
ngOnDestroy(): void {
this.gameInstance.Quit();
}

startStopRotating() {
this.gameInstance.SendMessage('Director', 'StartStopRotating');
}

startStopAnimation() {
this.gameInstance.SendMessage('Director', 'StartStopAnimation');
}

setDistance(distance: number) {
this.gameInstance.SendMessage('Director', 'SetDistance', distance);
}

/**
* 最后是移动来的全屏切换函数
*/
toggleFullScreen() {
this.gameInstance.SetFullscreen(1);
}

}

配置Angular引用外部js库:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"projects": {
"project-name": {
"architect": {
"build": {
"options": {
"scripts": [
"src/assets/Build/UnityLoader.js"
]
}
}
}
}
}
}

css可以引用assets中的,因此不用再考虑背景图片的问题
unity.component.css

1
@import "../../../../assets/TemplateData/style";

路由方面,找到一个xx-routing.module.ts文件,关联api路径和使用的component类即可

1
2
3
4
{
path: 'unity1',
component: UnityComponent,
},

2020版本整合前后

整合前

相比与2019版本,有了一些改进

  1. 将进度条显示的js集成到了模板文件中
  2. 使用了一些promise的写法,不再是单纯的函数调用
  3. 增加了一些浏览器检测的逻辑
  4. 更合理的html和css结构
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
<!DOCTYPE html>
<html lang="en-us">
<head>
<meta charset="utf-8">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Unity WebGL Player | DoctorNew</title>
<link rel="shortcut icon" href="TemplateData/favicon.ico">
<link rel="stylesheet" href="TemplateData/style.css">
</head>
<body>
<div id="unity-container" class="unity-desktop">
<canvas id="unity-canvas"></canvas>
<div id="unity-loading-bar">
<div id="unity-logo"></div>
<div id="unity-progress-bar-empty">
<div id="unity-progress-bar-full"></div>
</div>
<div id="unity-progress-value">0.0%</div>
</div>
<div id="unity-mobile-warning">
WebGL builds are not supported on mobile devices.
</div>
<div id="unity-footer">
<div id="unity-webgl-logo"></div>
<div id="unity-fullscreen-button"></div>
<div id="unity-build-title">DoctorNew</div>
</div>
</div>

<script>
const buildUrl = "Build";
const loaderUrl = buildUrl + "/output.loader.js";
const config = {
dataUrl: buildUrl + "/output.data",
frameworkUrl: buildUrl + "/output.framework.js",
codeUrl: buildUrl + "/output.wasm",
streamingAssetsUrl: "StreamingAssets",
companyName: "DefaultCompany",
productName: "DoctorNew",
productVersion: "0.1",
};

const container = document.querySelector("#unity-container");
const canvas = document.querySelector("#unity-canvas");
const loadingBar = document.querySelector("#unity-loading-bar");
const progressBarFull = document.querySelector("#unity-progress-bar-full");
const progressValue = document.querySelector("#unity-progress-value");
const fullscreenButton = document.querySelector("#unity-fullscreen-button");
const mobileWarning = document.querySelector("#unity-mobile-warning");

if (/iPhone|iPad|iPod|Android/i.test(navigator.userAgent)) {
container.className = "unity-mobile";
config.devicePixelRatio = 1;
mobileWarning.style.display = "block";
setTimeout(() => {
mobileWarning.style.display = "none";
}, 5000);
} else {
canvas.style.width = "99vw";
canvas.style.height = "99vh";
}
loadingBar.style.display = "block";

const script = document.createElement("script");
script.src = loaderUrl;
script.onload = () => {
createUnityInstance(canvas, config, (progress) => { // 这里使用了一个外部库函数
progressBarFull.style.width = 100 * progress + "%";
progressValue.textContent = (100 * progress).toFixed(1) + "%";
}).then((unityInstance) => {
loadingBar.style.display = "none";
fullscreenButton.onclick = () => {
unityInstance.SetFullscreen(1);
// document.makeFullscreen('unity-container');
};
}).catch((message) => {
alert(message);
});
};
document.body.appendChild(script);
</script>
</body>
</html>

整合后

首先引用外部库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"projects": {
"project-name": {
"architect": {
"build": {
"options": {
"scripts": [
"src/assets/Build/xxx.loader.js"
]
}
}
}
}
}
}

然后思考重写一些逻辑时的关键变量

  1. 进度数值(用于控制进度条宽度)
  2. 是否已经加载成功
  3. 是否移动端

将这些反映在模板和组件类中即可

1
2
3
4
5
6
7
8
9
10
11
12
13
<ng-container>
<div id="unity-container">
<canvas id="unity-canvas" #unityCanvas></canvas> <!-- 注意这里为了让组件类引用页面元素,打了一个标记 -->

<div id="unity-loading-bar" *ngIf="!isReady"> <!-- 控制进度条显示 -->
<div id="unity-logo"></div>
<div id="unity-progress-bar-empty">
<div id="unity-progress-bar-full" [style.width.%]="progress*100"></div> <!-- 控制进度显示 -->
</div>
<div id="unity-progress-value">{{(progress * 100).toFixed(1)}}%</div>
</div>
</div>
</ng-container>

组件类如下

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
import { Component, AfterViewInit, OnDestroy, ViewChild, ElementRef } from '@angular/core';
declare const createUnityInstance: any; // 这里引用了外部库 xxx.loader.js里的函数

@Component({
selector: 'unity',
templateUrl: './unity.component.html',
styleUrls: ['./unity.component.css']
})
export class UnityComponent implements AfterViewInit, OnDestroy {

@ViewChild('unityCanvas',{static: false}) canvas: ElementRef; // 引用一个页面元素的方法

gameInstance: any; // 用于销毁实例,可能以后就不用了
progress = 0;
isReady = false;
isMobile = false;

constructor() { }

//该生命周期函数只触发一次
ngAfterViewInit(){ // 须在view初始化完成后才能找到要操作的canvas对象

const buildUrl = "/assets/Build";
const config = {
dataUrl: buildUrl + "/output.data",
frameworkUrl: buildUrl + "/output.framework.js",
codeUrl: buildUrl + "/output.wasm",
streamingAssetsUrl: "StreamingAssets",
companyName: "DefaultCompany",
productName: "xxx",
productVersion: "0.1",
};

this.canvas.nativeElement.style.width = "960px";
this.canvas.nativeElement.style.height = "600px";

// 注意这里引用html元素的方法
createUnityInstance(this.canvas.nativeElement, config, (progress) => {
this.progress = progress;
}).then((unityInstance) => {
this.isReady = true;
this.gameInstance = unityInstance;
}).catch((message) => {
alert(message);
});
}

ngOnDestroy(): void {
this.gameInstance.Quit();
}

startStopRotating() {
this.gameInstance.SendMessage('Director', 'StartStopRotating');
}

startStopAnimation() {
this.gameInstance.SendMessage('Director', 'StartStopAnimation');
}

setDistance(distance: number) {
this.gameInstance.SendMessage('Director', 'SetDistance', distance);
}

toggleFullScreen() {
this.gameInstance.SetFullscreen(1);
}

}

附带一个能用的css

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
body { padding: 0; margin: 0 }
#unity-container { position: absolute }
#unity-container.unity-desktop { left: 50%; top: 50%; transform: translate(-50%, -50%) }
#unity-container.unity-mobile { width: 100%; height: 100% }
#unity-canvas { background: #231F20 }
.unity-mobile #unity-canvas { width: 100%; height: 100% }
#unity-loading-bar { position: absolute; left: 50%; top: 50%; width:450px; transform: translate(-50%, -50%) } /* 要注意去掉display:none */
#unity-logo { height: 130px; background: url('unity-logo-dark.png') no-repeat center } /* logo设置了宽度会让图标不居中 */
#unity-progress-bar-empty { width: 88%; height: 18px; margin-top: 10px; float:left; background: url('progress-bar-empty-dark.png') no-repeat; background-size: 100% 18px }
#unity-progress-bar-full { width: 0%; height: 18px; background: url('progress-bar-full-dark.png')}
#unity-progress-value {width: 12%; height: 18px; font-size: 15px; margin-top: 10px; float: right; color: white; text-align: right}
#unity-footer { position: relative }
.unity-mobile #unity-footer { display: none }
#unity-webgl-logo { float:left; width: 204px; height: 38px; background: url('webgl-logo.png') no-repeat center }
#unity-build-title { float: right; margin-right: 10px; line-height: 38px; font-family: arial; font-size: 18px }
#unity-fullscreen-button { float: right; width: 38px; height: 38px; background: url('fullscreen-button.png') no-repeat center }
#unity-mobile-warning { position: absolute; left: 50%; top: 5%; transform: translate(-50%); background: white; padding: 10px; display: none }

参考

  1. 看起来是一个主要写C#和asp.net的大叔的博客
  2. 他的代码地址