1
0
hugo/content/blog/2023-11-04-vrm.md
2024-04-23 22:21:26 +09:00

347 lines
9.4 KiB
Markdown

+++
date = "2023-11-04"
tags = ["vrm","3d"]
title = "3d-modelとcardを連携してみた"
slug = "vrm"
+++
https://card.syui.ai/ai
### 動作環境
ios17で動作します。ios16では動作しません。
- [ok] ... ios17.x
- [no] ... ios16.x
`safari`の以下の機能が必要です。
ios17のデフォルトでは有効になっています。
- Allow WebGL in Web Workers
- GPU Process: Canvas Rendering
- GPU Process: DOM Rendering
- OffscreenCanvas in Workers
- OffscreenCanvas
### fix motion
例えば、以下はblenderでポーズを編集している様子なんだけど、プレビューと出力結果が異なります。
![](https://raw.githubusercontent.com/syui/img/master/other/blender_20230908_0005.png)
![](https://raw.githubusercontent.com/syui/img/master/other/blender_20230908_0006.png)
これは、アニメーションが自動で動作するように設定されているためです。そのままvrmを読み込むとTポーズになりますが、[JLChnToZ/vrm-dance-viewer](https://github.com/JLChnToZ/vrm-dance-viewer)は設定で手を下げて固定します。
```ts:src/worker/vrm-idle-helper.ts
// https://github.com/JLChnToZ/vrm-dance-viewer
const LERP_SCALE = 6;
if (node) node.setRotationFromQuaternion(
rotation3
.setFromRotationMatrix(node.matrix)
// ここがポーズをおかしくする要因
// コメント化すると元通り。ただvrmを自然なポーズに固定する必要がでてくる
//.slerp(finalRotation, Math.min(deltaTime * LERP_SCALE, 1)),
);
```
また、blinkを設定すると、顔を標準位置から移動するとおかしくなります。これは`three-vrm`の古いバージョンのバグです。
```ts:src/worker/vrm-idle-helper.ts
function updateEyeBlink(model: VRM, deltaTime: number) {
//if (!model.blendShapeProxy) return;
//let v = blinkDelays.get(model);
//if (v == null || v < -BLINK_DURATION)
// v = MathUtils.randFloat(MIN_BLINK_DELAY, MAX_BLINK_DEALY);
//else
// v -= deltaTime;
//blinkDelays.set(model, v);
//model.blendShapeProxy.setValue(
// VRMSchema.BlendShapePresetName.Blink,
// v > LOOK_CAMERA_THRESHOLD ? 0 : MathUtils.pingpong(-v, BLINK_DURATION / 2) * 2 / BLINK_DURATION,
//);
}
```
これをどう自然に動かしていけばいいのか悩み中。全部のvrmをデフォルトのTポーズから変更するのもあんまり良くない。
`three.js`の他に`babylon.js`というものもあるらしい。こちらの[virtual-cast/babylon-vrm-loader](https://github.com/virtual-cast/babylon-vrm-loader)でvrmを読み込めます。
### add effect
例えば、model/motionを読み込んだ瞬間に紙吹雪が舞う演出を追加してみます。
```js:src/worker/scene/lights.ts
// 空中に紙吹雪
function tick_sky() {
let s_rot = 0;
let s_xp = 10;
let s_yp = 10;
let s_zp = 10;
const s_length = 5000;
const s_plane_scale = 0.01;
const s_plane = [];
for(let i=0; i<s_length; i++){
var color = "0x" + Math.floor(Math.random() * 16777215).toString(16);
let geometry = new THREE.PlaneGeometry( s_plane_scale, s_plane_scale );
var material = new THREE.MeshBasicMaterial({
color: Number(color),
opacity: 0.8,
transparent: true,
side: THREE.DoubleSide
});
s_plane[i] = new THREE.Mesh( geometry, material );
s_plane[i].position.x = s_xp * (Math.random() - 0.5);
s_plane[i].position.y = s_yp * (Math.random() - 0.5);
s_plane[i].position.z = s_zp * (Math.random() - 0.5);
scene.add(s_plane[i]);
}
return s_plane;
}
// 紙吹雪を時間経過で消す処理
function tick_sky_remove(){
const s_length = 5000;
var s_plane = tick_sky();
setTimeout(() => {
for(let i=0; i<s_length; i++){
scene.remove(s_plane[i]);
}
}, 7000);
}
// サービス名を追加
export function toggleTickSky() {
if (!navigator.userAgent.match(/iPhone|iPod|iPad|Android.+Mobile/)) {
tick_sky_remove();
}
}
// ホストへ追加
WorkerMessageService.host.on({ setLights, toggleLights, toggleTickSky });
```
```js:src/host/index.ts
// サービス登録
export function toggleTickSky() {
return void workerService.trigger('toggleTickSky');
}
```
```js:src/main.ts
import { toggleTickSky } from './host';
const el = document.querySelector('#btn-models') as HTMLInputElement | null;
if(el != null) {
// ボタンを押したときの動作
el.addEventListener('click', function(){
// サービス:紙吹雪の演出
toggleTickSky();
// サービス:ライトのon/off
toggleLights();
});
}
```
```html:src/index.html
<button id='btn-models'>test</button>
```
このようにするとmodel/motionにeffectを追加できます色々な`scene`を作っていく予定
<video controls style="width:100%;"><source src="/ai/video/ai_vrm_0002.mp4"></video>
### add audio
```js:src/main.ts
import * as THREE from 'three';
export const camera = new THREE.PerspectiveCamera(75, 1, 0.1, 1000);
const listener = new THREE.AudioListener();
camera.add( listener );
const sound = new THREE.Audio( listener );
const audioLoader = new THREE.AudioLoader();
function audio_sword() {
audioLoader.load( './audio/sword.mp3', function( buffer ) {
sound.setBuffer( buffer );
sound.setLoop( false );
sound.setVolume( 0.2 );
sound.play();
});
}
audio_sword();
```
### add floor
先程の応用でランダムで動き続ける背景を設定してみます。
```js:src/worker/scene/lights.ts
function tick() {
let s_rot = 0;
let s_xp = 30;
// 0にして床のみ設定する
let s_yp = 0;
let s_zp = 30;
const s_length = 15000;
// 大きさもランダムにする
const s_plane_scale = Math.floor(Math.random() * 0.09) + 0.01;
const s_plane = [];
for(let i=0; i<s_length; i++){
var color = "0x" + Math.floor(Math.random() * 16777215).toString(16);
let geometry = new THREE.PlaneGeometry( s_plane_scale, s_plane_scale );
var material = new THREE.MeshBasicMaterial({
color: Number(color),
opacity: 0.8,
transparent: true,
side: THREE.DoubleSide
});
s_plane[i] = new THREE.Mesh( geometry, material );
// 向きをランダムに変える
s_plane[i].rotation.x = Math.PI / 2 * Math.random();
s_plane[i].position.x = s_xp * (Math.random() - 0.5);
s_plane[i].position.y = s_yp * (Math.random() - 0.5);
s_plane[i].position.z = s_zp * (Math.random() - 0.5);
scene.add(s_plane[i]);
}
return s_plane;
}
function tick_remove(){
const s_length = 15000;
var s_plane = tick();
setTimeout(() => {
for(let i=0; i<s_length; i++){
scene.remove(s_plane[i]);
}
}, 2000);
}
// これで床のキラキラが更新を続ける
export function toggleTick() {
const s = tick();
tick_remove();
var num = Math.floor(Math.random() * 1990) + 1790;
for(let i=0; i<100; i++){
var num = Math.floor(Math.random() * 1990) + 1790;
setTimeout(() => { tick_remove(); }, num * i);
}
}
```
### add hdr
hdr画像を設定します。ただし、backgroundはつけないでください。床はつけてもいいです。
![](https://raw.githubusercontent.com/syui/img/master/other/ai_vrm_0010.png)
```js
//背景をHDR
import * as THREE from 'three';
import { RGBELoader } from "three/examples/jsm/loaders/RGBELoader";
let uk_0 = "/img/t.hdr"
new RGBELoader().load(uk_0, function (texture) {
texture.mapping = THREE.EquirectangularReflectionMapping;
scene.background = texture;
//scene.environment = texture;
});
// scene.background.set(bgColor);
```
hdr : https://polyhaven.com/hdris
ボタンを押すと場所を移動するように設定するにはこんな感じ。
```js:src/worker/scene/lights.ts
const bgColor = new Color(0x000000);
function getTels(){
let hdr = "/img/syferfontein_0d_clear_puresky_4k.hdr";
new RGBELoader().load(hdr, function (texture) {
texture.mapping = THREE.EquirectangularReflectionMapping;
scene.background = texture;
//scene.environment = texture;
});
}
function floor_default(){
const floor = new Mesh(
new PlaneBufferGeometry(100, 100),
new MeshLambertMaterial({
color: 0x999999,
depthWrite: true,
})
);
floor.position.y = -0.5;
floor.rotation.x = -Math.PI / 2;
//const { y } = floor.position;
//floor.position.set(0, 0, 0);
scene.add(floor);
return floor;
}
function floor_grid(){
const grid = new GridHelper(50, 100, 0xAAAAAA, 0xAAAAAA);
scene.add(grid);
grid.position.set(Math.round(0), 0, Math.round(0));
return grid;
}
function floor_bg(){
const bgColor = new Color(0xffffff);
scene.background = new Color(bgColor);
scene.fog = new Fog(bgColor, 3, 10);
scene.fog?.color.set(bgColor);
if (scene.background instanceof Color)
scene.background.set(bgColor);
else
scene.background = bgColor.clone();
}
export const fl_de = floor_default();
export const fl_gr = floor_grid();
export const fl_bg = floor_bg();
function floor_default_remove(int: number){
if (int == 1){
scene.remove(fl_de);
scene.remove(fl_gr);
getTels();
}
}
floor_default_remove(0);
export function toggleTel(int: number) {
floor_default_remove(int)
}
WorkerMessageService.host.on({ toggleTel });
```
```js:src/host/index.ts
export function toggleTel(int: number) {
return void workerService.trigger('toggleTel', [int]);
}
```
```js:src/main.ts
import { toggleTel } from './host';
const el_tel = document.querySelector('#btn-models_tel') as HTMLInputElement | null;
if(el_tel != null) {
el_tel.addEventListener('click', (e:Event) => toggleTel(1));
}
```
### link
- https://zenn.dev/ikkou/articles/fdb344a713cdf0/
- https://blog.virtualcast.jp/blog/2019/05/oss-browser-vrm-vci-viewer/