오디오 로드 및 재생
애니메이션과 동기화된 오디오를 재생해 보겠습니다.
MP3 오디오 파일 다운로드
먼저 오디오 파일이 필요합니다. 사용 중인 애니메이션의 오디오는 higma - メランコリ・ナイト / melancholy night feat.初音ミク입니다.
YouTube to MP3 컨버터 또는 유사한 도구를 사용해 MP3 파일로 다운로드할 수 있습니다.
다운로드한 MP3 파일을 "res/private_test/motion/メランコリ・ナイト/" 폴더에 배치하세요.
폴더 구조 예시
스트림 오디오 플레이어 생성
스트림 오디오 플레이어를 생성합니다. 이 플레이어는 HTMLAudioElement를 사용하여 스트리밍 방식으로 오디오를 재생합니다.
src/sceneBuilder.ts
import { StreamAudioPlayer } from "babylon-mmd/esm/Runtime/Audio/streamAudioPlayer";
//...
export class SceneBuilder implements ISceneBuilder {
public async build(_canvas: HTMLCanvasElement, engine: AbstractEngine): Promise<Scene> {
//...
const audioPlayer = new StreamAudioPlayer(scene);
audioPlayer.source = "res/private_test/motion/メランコリ・ナイト/melancholy_night.mp3";
//...
}
}
그런 다음 런타임에 오디오 플레이어를 설정합니다.
src/sceneBuilder.ts
//...
import { MmdRuntime } from "babylon-mmd/esm/Runtime/mmdRuntime";
//...
export class SceneBuilder implements ISceneBuilder {
public async build(_canvas: HTMLCanvasElement, engine: AbstractEngine): Promise<Scene> {
//...
const mmdRuntime = new MmdRuntime(scene);
mmdRuntime.loggingEnabled = true;
mmdRuntime.register(scene);
mmdRuntime.setAudioPlayer(audioPlayer);
mmdRuntime.playAnimation();
//...
}
}
오디오는 애니메이션이 부드럽게 재생되도록 playAnimation
호출 전에 설정해야 합니다.
애니메이션 재생이 시작된 후에 오디오 플레이어를 설정하면 오디오와 동기화하는 동안 짧은 끊김이 발생할 수 있습니다.
전체 코드
src/sceneBuilder.ts
import "@babylonjs/core/Lights/Shadows/shadowGeneratorSceneComponent";
import "babylon-mmd/esm/Loader/pmxLoader";
import "babylon-mmd/esm/Loader/mmdOutlineRenderer";
import "babylon-mmd/esm/Runtime/Animation/mmdRuntimeCameraAnimation";
import "babylon-mmd/esm/Runtime/Animation/mmdRuntimeModelAnimation";
import type { AbstractEngine } from "@babylonjs/core/Engines/abstractEngine";
import { DirectionalLight } from "@babylonjs/core/Lights/directionalLight";
import { ShadowGenerator } from "@babylonjs/core/Lights/Shadows/shadowGenerator";
import { LoadAssetContainerAsync } from "@babylonjs/core/Loading/sceneLoader";
import { Color3, Color4 } from "@babylonjs/core/Maths/math.color";
import { Vector3 } from "@babylonjs/core/Maths/math.vector";
import { CreateGround } from "@babylonjs/core/Meshes/Builders/groundBuilder";
import { Scene } from "@babylonjs/core/scene";
import { MmdStandardMaterialBuilder } from "babylon-mmd/esm/Loader/mmdStandardMaterialBuilder";
import { VmdLoader } from "babylon-mmd/esm/Loader/vmdLoader";
import { StreamAudioPlayer } from "babylon-mmd/esm/Runtime/Audio/streamAudioPlayer";
import { MmdCamera } from "babylon-mmd/esm/Runtime/mmdCamera";
import type { MmdMesh } from "babylon-mmd/esm/Runtime/mmdMesh";
import { MmdRuntime } from "babylon-mmd/esm/Runtime/mmdRuntime";
import type { ISceneBuilder } from "./baseRuntime";
export class SceneBuilder implements ISceneBuilder {
public async build(_canvas: HTMLCanvasElement, engine: AbstractEngine): Promise<Scene> {
const materialBuilder = new MmdStandardMaterialBuilder();
const scene = new Scene(engine);
scene.clearColor = new Color4(0.95, 0.95, 0.95, 1.0);
scene.ambientColor = new Color3(0.5, 0.5, 0.5);
const mmdCamera = new MmdCamera("MmdCamera", new Vector3(0, 10, 0), scene);
const directionalLight = new DirectionalLight("DirectionalLight", new Vector3(0.5, -1, 1), scene);
directionalLight.intensity = 1.0;
directionalLight.autoCalcShadowZBounds = true;
const shadowGenerator = new ShadowGenerator(1024, directionalLight, true);
shadowGenerator.transparencyShadow = true;
shadowGenerator.usePercentageCloserFiltering = true;
shadowGenerator.forceBackFacesOnly = true;
shadowGenerator.filteringQuality = ShadowGenerator.QUALITY_MEDIUM;
shadowGenerator.frustumEdgeFalloff = 0.1;
const ground = CreateGround("ground1", { width: 100, height: 100, subdivisions: 2, updatable: false }, scene);
ground.receiveShadows = true;
const audioPlayer = new StreamAudioPlayer(scene);
audioPlayer.source = "res/private_test/motion/メランコリ・ナイト/melancholy_night.mp3";
const vmdLoader = new VmdLoader(scene);
vmdLoader.loggingEnabled = true;
const mmdRuntime = new MmdRuntime(scene);
mmdRuntime.loggingEnabled = true;
mmdRuntime.register(scene);
mmdRuntime.setAudioPlayer(audioPlayer);
mmdRuntime.playAnimation();
const [mmdAnimation, modelMesh] = await Promise.all([
vmdLoader.loadAsync("motion",
[
"res/private_test/motion/メランコリ・ナイト/メランコリ・ナイト_カメラ.vmd",
"res/private_test/motion/メランコリ・ナイト/メランコリ・ナイト_表情モーション.vmd",
"res/private_test/motion/メランコリ・ナイト/メランコリ・ナイト_リップモーション.vmd",
"res/private_test/motion/メランコリ・ナイト/メランコリ・ナイト.vmd"
]),
LoadAssetContainerAsync(
"res/private_test/model/YYB Hatsune Miku_10th/YYB Hatsune Miku_10th_v1.02.pmx",
scene,
{
pluginOptions: {
mmdmodel: {
loggingEnabled: true,
materialBuilder: materialBuilder
}
}
}
).then(result => {
result.addAllToScene();
return result.rootNodes[0] as MmdMesh;
})
]);
const cameraAnimationHandle = mmdCamera.createRuntimeAnimation(mmdAnimation);
mmdCamera.setRuntimeAnimation(cameraAnimationHandle);
mmdRuntime.addAnimatable(mmdCamera);
{
for (const mesh of modelMesh.metadata.meshes) mesh.receiveShadows = true;
shadowGenerator.addShadowCaster(modelMesh);
const mmdModel = mmdRuntime.createMmdModel(modelMesh);
const modelAnimationHandle = mmdModel.createRuntimeAnimation(mmdAnimation);
mmdModel.setRuntimeAnimation(modelAnimationHandle);
}
return scene;
}
}