Open7

PlayCanvas便利スクリプト

はがはが

非同期でのシーン移動

  async loadScene(sceneName) {
    const oldHierarchy = this.app.root.findByName("Root");
    const scene = this.app.scenes.find(sceneName);
    return new Promise((resolve, reject) => {
      this.app.scenes.loadSceneHierarchy(scene.url, (err, parent) => {
        if (!err) {
          resolve(oldHierarchy);
        } else {
          reject();
        }
      });
    });
  }
}

使用例

 async changeVrScene(sceneName) {
    this.app.root.findByName("CameraParent").setPosition(0, 1.5, 0);
    const old = await this.loadScene(sceneName);
    old.destroy();
  }
はがはが

円を書く

var CircleEntityCreator = pc.createScript('circleEntityCreator');
CircleEntityCreator.attributes.add("createEntity", { type: "entity" });
CircleEntityCreator.attributes.add("itemCount", { type: "number", default: 40 });
CircleEntityCreator.attributes.add("radius", { type: "number", default: 5 });
CircleEntityCreator.attributes.add("repeat", { type: "number", default: 2 });

CircleEntityCreator.prototype.initialize = function () {
    const oneCycle = 2.0 * Math.PI;

    for (let i = 0; i < this.itemCount; i++) {

        const point = (i / this.itemCount) * oneCycle; 
        const repeatPoint = point * this.repeat;

        const x = Math.cos(repeatPoint) * this.radius;
        const y = Math.sin(repeatPoint) * this.radius;

        const position = new pc.Vec3(x, y);

        const entity = new pc.Entity();
        entity.addComponent("render", {
            type: "box"
        });
        entity.setLocalPosition(position);
        this.entity.addChild(entity);
    }
};

参考にさせていただいたページ

円状にオブジェクトを生成する
https://qiita.com/beckyJPN/items/e9c90ea3943866cb6e18

はがはが

Render To Texture

https://playcanvas.github.io/#/graphics/render-to-texture

PlayCanvas Editor用スクリプト

var TextureScript = pc.createScript('textureScript');

// script attributes for material
TextureScript.attributes.add('material', {
    type: 'asset',
    assetType: 'material'
});

TextureScript.prototype.initialize = function() {
    var app = this.app;

    // create texture and render target for rendering into
    const texture = new pc.Texture(app.graphicsDevice, {
        width: 512,
        height: 512,
        format: pc.PIXELFORMAT_RGB8,
        mipmaps: true,
        minFilter: pc.FILTER_LINEAR,
        magFilter: pc.FILTER_LINEAR,
        addressU: pc.ADDRESS_CLAMP_TO_EDGE,
        addressV: pc.ADDRESS_CLAMP_TO_EDGE,
    });

    const renderTarget = new pc.RenderTarget({
        name: `camera-to-texture`,
        colorBuffer: texture,
        depth: true,
        flipY: true,
        samples: 2,
    });

    // set renderTarget to the camera component
    this.entity.camera.renderTarget = renderTarget;
    this.material.resource.emissiveMap = texture;
    this.material.resource.diffuseMap = texture;
    this.material.resource.update();
    // get world and skybox layers
    const worldLayer = app.scene.layers.getLayerByName("World");
    const skyboxLayer = app.scene.layers.getLayerByName("Skybox");

    // set layers of this camera
    this.entity.camera.layers = [worldLayer.id, skyboxLayer.id];
};

// call this function when material attribute changes
TextureScript.prototype.onAttributeChanged = function(name, oldValue, newValue) {
    if(name === 'material' && newValue) {
        var materialAsset = this.app.assets.get(newValue);
        if (materialAsset) {
            // change material of render component
            this.entity.render.material = materialAsset.resource;
        }
    }
};
はがはが

PlayCanvas用にモーフ(シェイプキー)をBlenderから全て書き出すためのPythonスクリプト

  1. シェイプキーのあるオブジェクト(Body / Faceなど)を選択
  2. BlenderのScriptを実行するとGLB形式ですべてアニメーション付きのGLBがBlenderから出力されます。
import bpy

# オブジェクトを選択します。
obj = bpy.context.object

# シェイプキーのリストを取得します。
shape_keys = obj.data.shape_keys.key_blocks

# ループの回数を制限します。
loop_count = min(5, len(shape_keys) - 1)

# 各シェイプキーに対してアニメーションを作成し、GLBとしてエクスポートします。
for key in shape_keys[1:1+loop_count]:  # Basisはスキップし、ループを5回までに制限します。
    
    # 新しいアクションを作成してアサイン
    action_name = f"Action_{key.name}"
    action = bpy.data.actions.new(action_name)
    obj.data.shape_keys.animation_data_create()
    obj.data.shape_keys.animation_data.action = action
    
    # シェイプキーの値をすべて0にリセット
    for k in shape_keys[1:]:
        k.value = 0.0
        k.keyframe_insert(data_path="value", index=-1, frame=0)
    
    # ターゲットとなるシェイプキーのアニメーションを設定
    key.value = 1.0
    key.keyframe_insert(data_path="value", index=-1, frame=24)
    
    # GLBとしてエクスポート
    export_path = f"your_directory_path/{key.name}.glb"  # 保存先のパスを設定してください
    bpy.ops.export_scene.gltf(filepath=export_path, export_format='GLB')
    
    # 作成したアクションを削除
    bpy.data.actions.remove(action)
  1. PlayCanvasのAnim State Graphで設定します。
はがはが

シーンのデータを元に360度動画の撮影をする

https://www.youtube.com/watch?v=K0Mq8Wfv5dI

PlayCanvasのカメラを使って360度動画を撮影する方法を紹介します。
プロジェクトの元になるのは、以下のPublicプロジェクトです。

https://playcanvas.com/project/1203128

実行URLはこちらです。
https://playcanv.as/p/wrwiuEbZ/

360度動画撮影の仕組み

プロジェクト内のコードは、正距円筒図法(Equirectangular projection)でカメラを描画をすることで作成ができました。実装は、PlayCanvasのReflection Cubemapのデモを元に実装をしたコードがこちらです。

const EquirectangularCamera = pc.createScript('equirectangular-camera');

EquirectangularCamera.attributes.add("srcSphere", { type: "entity" });

EquirectangularCamera.prototype.postInitialize = function () {
    this.textureEqui = this.createReprojectionTexture(pc.TEXTUREPROJECTION_EQUIRECT, 2048);
};

EquirectangularCamera.prototype.postUpdate = function () {
    pc.reprojectTexture(this.srcSphere.script.cubemapRenderer.cubeMap, this.textureEqui, { numSamples: 1 });
    this.app.drawTexture(0, 0, 2, 2, this.textureEqui);
}

EquirectangularCamera.prototype.createReprojectionTexture = function (projection, size) {
    return new pc.Texture(this.app.graphicsDevice, {
        width: size,
        height: size,
        format: pc.PIXELFORMAT_RGB8,
        mipmaps: false,
        minFilter: pc.FILTER_LINEAR,
        magFilter: pc.FILTER_LINEAR,
        addressU: pc.ADDRESS_CLAMP_TO_EDGE,
        addressV: pc.ADDRESS_CLAMP_TO_EDGE,
        projection: projection
    });
};

このプロジェクトをフォークするか、一部を使用することで360度動画を撮影することができます。

プロジェクトを移植する際の設定

1. レイヤーの設定

  1. 新規にレイヤー(excludedLayer)を作成します。
  2. レイヤーを適切に設定します。

2. ヒエラルキーの設定

  1. カメラにequirectangular-camera.jsを設定します。
  2. カメラの子エンティティ(Sphere)を追加し、cubemap-renderer.jsを設定します。

それぞれカメラのエンティティとカメラの子エンティティの設定はこちらです。

カメラの設定

カメラの子エンティティの設定

3. 解像度の設定

PlayCanvasの設定から、解像度とFill Modeを設定します。
これにより、起動時にウィンドウのサイズに関わらずアスペクト比を固定することができます。

YouTubeにアップロードをする

設定が完了したら、F11キーを押してシーンを全画面表示にし、録画を行います。

YouTubeにアップロードする場合は、撮影した動画(mp4 / mov形式)に対して「Spatial Media Metadata Injector」を使ってメタデータを付与することで、360度動画としてアップロードできます。

はがはが

特定のタグのアセットをロード、アンロードを動的に行う方法。
ヒエラルキーにあるRenderコンポーネントを使ったエンティティで使用しているアセットをUnloadをするには、Texture / Render / Containerにそれぞれの共通のタグを付けることでリソースの開放ができます。

コード 1 アセットのロード / アンロード

var AssetManager = pc.createScript('assetManager');

AssetManager.prototype.initialize = function() {
    // アセットロードイベントのリスナーを設定
    this.app.on('loadAsset', this.loadAssetWithTag, this);

    // アセットアンロードイベントのリスナーを設定
    this.app.on('unloadAsset', this.unloadAssetWithTag, this);
};

// タグに基づいてアセットをロードする関数
AssetManager.prototype.loadAssetWithTag = function(tag) {
    var assets = this.app.assets.findByTag(tag);
    assets.forEach((asset) => {
        asset.ready(() => {
            console.log('Asset loaded: ' + asset.name);
        });
        asset.load();
    });
};

// タグに基づいてアセットをアンロードする関数
AssetManager.prototype.unloadAssetWithTag = function(tag) {
    var assets = this.app.assets.findByTag(tag);
    assets.forEach((asset) => {
        asset.unload();
        console.log('Asset unloaded: ' + asset.name);
    });
};

コード 2 アセットのロード / アンロードを順次行う

var AssetManager = pc.createScript('assetManager');

// タグリストと現在のタグインデックス、そして一度にロード/アンロードするアセットの数を定義
AssetManager.attributes.add('tagList', { type: 'string', array: true });
AssetManager.attributes.add('loadUnloadCount', { type: 'number', default: 1 });

AssetManager.prototype.initialize = function () {
    this.currentTagIndex = 0;
    this.app.on('loadNext', this.handleLoadNext, this);
    setInterval(() => {
        this.app.fire("loadNext");
    }, 1000)
};

// 次のアセットをロードするための関数
AssetManager.prototype.handleLoadNext = function () {
    var totalTags = this.tagList.length;
    var unloadIndex = this.currentTagIndex;

    // 現在のタグのアセットをアンロード
    for (let j = 0; j < this.loadUnloadCount; j++) {
        var currentUnloadTag = this.tagList[unloadIndex % totalTags];
        this.unloadAssetsWithTag(currentUnloadTag);
        unloadIndex++;
    }

    // インデックスを更新
    this.currentTagIndex = (this.currentTagIndex + this.loadUnloadCount) % totalTags;

    // 次のタグのアセットをロード
    var loadIndex = this.currentTagIndex;
    for (let i = 0; i < this.loadUnloadCount; i++) {
        var currentLoadTag = this.tagList[loadIndex % totalTags];
        this.loadAssetsWithTag(currentLoadTag);
        loadIndex++;
    }
};

// 特定のタグのアセットをロードする関数
AssetManager.prototype.loadAssetsWithTag = function (tag) {
    var assets = this.app.assets.findByTag(tag);
    assets.forEach((asset) => {
        asset.ready(() => {
            console.log('Asset loaded: ' + asset.name);
        });
        this.app.assets.load(asset);
    });
};

// 特定のタグのアセットをアンロードする関数
AssetManager.prototype.unloadAssetsWithTag = function (tag) {
    var assets = this.app.assets.findByTag(tag);
    assets.forEach((asset) => {
        asset.unload();
        console.log('Asset unloaded: ' + asset.name);
    });
};