Three.jsでMeshやGroupを複製する基本はObject3D.clone()です。
const clonedMesh = originalMesh.clone();
clonedMesh.position.x = 3;
scene.add(clonedMesh);
ただし、Mesh.clone()は「すべてのresourceを完全に独立copyするmethod」ではありません。position・rotation・scaleや子Object3Dは複製されますが、GeometryとMaterialは通常同じinstanceを共有します。
console.log(originalMesh.geometry === clonedMesh.geometry); // true
console.log(originalMesh.material === clonedMesh.material); // true
この記事では、Object3D・Mesh・Group・Material・Geometry・Texture・glTFを目的に応じて複製する方法と、SkinnedMesh・InstancedMesh・disposeの注意点を解説します。
clone()でMeshを複製する基本
最小例です。
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshStandardMaterial({ color: 0x38bdf8 });
const original = new THREE.Mesh(geometry, material);
original.position.x = -1.5;
const clone = original.clone();
clone.position.x = 1.5;
scene.add(original, clone);
cloneには新しいObject3Dとして別のuuidが付きます。position・rotation・scaleも別instanceなので、cloneの位置を変えてもoriginalの位置は変わりません。
Scene、Camera、Rendererを含む最小構成から確認したい場合はThree.jsの始め方を先に参照してください。
代入・add・cloneの違い
JavaScriptの代入は複製ではなく、同じobjectへのreferenceを増やします。
const meshA = original;
const meshB = original;
meshB.position.x = 5;
console.log(meshA.position.x); // 5
meshAとmeshBは同じMeshです。
また、Object3Dは同時に1つのparentだけを持ちます。同じMeshを別Groupへaddすると、元のparentから外れて新しいparentへ移動します。
groupA.add(original);
groupB.add(original); // originalはgroupAから外れてgroupBへ移る
同じ見た目のobjectを複数parentへ同時表示したい場合はcloneを作ります。
Mesh.clone()で独立・共有されるもの
Mesh cloneの設計を表で整理します。
| data | default | 片方の変更 |
|---|---|---|
| Object3D本体・uuid | 独立 | もう片方へ影響しない |
| position / rotation / scale | 値をcopyした独立instance | もう片方へ影響しない |
| children | recursive=trueなら子もclone | scene graph nodeは独立 |
| Geometry | 同じreferenceを共有 | 頂点変更・disposeが他へ影響 |
| Material | 同じreferenceを共有 | 色・opacity変更が他へ影響 |
| Texture | Material経由で共有 | wrap・offset・disposeが他へ影響 |
| AnimationClip | 共有可能 | Mixer・action stateはcloneごとに管理 |
recursiveはObject3D階層を再帰的にcloneする指定であり、Geometry・Material・Textureまでdeep cloneする意味ではありません。
clone(true)とclone(false)の違い
Object3D.clone()の引数は、子孫も複製するかを決めます。defaultはtrueです。
const withChildren = group.clone(true);
const withoutChildren = group.clone(false);
clone(true): Group以下の子Object3Dを再帰的にcloneclone(false): root Object3Dだけをclone
Group内のMeshは新しいMesh nodeになりますが、そのMeshが持つGeometry・Materialは元Meshと共有されます。
Groupを階層ごと複製する
複数partで構成したobjectはGroup rootをcloneします。
const robot = new THREE.Group();
const body = new THREE.Mesh(bodyGeometry, bodyMaterial);
body.name = "body";
const head = new THREE.Mesh(headGeometry, headMaterial);
head.name = "head";
head.position.y = 1.5;
robot.add(body, head);
const robotClone = robot.clone(true);
robotClone.position.x = 4;
scene.add(robot, robotClone);
clone後の子はgetObjectByName()で取得できます。
const clonedHead = robotClone.getObjectByName("head");
clonedHead.rotation.y = Math.PI / 4;
nameはcopyされるため、同じScene内でnameが重複します。uuidは別ですが、nameをunique identifierとして使う設計ではclone後に付け直します。
copy()で既存Object3Dへ値を複製する
clone()は新しいinstanceを作ります。copy()は既存instanceへsourceの値をcopyします。
const target = new THREE.Object3D();
target.copy(source, true);
独自classでconstructor処理を保ちながらObject3D propertyをcopyしたい場合などに使えます。
class Marker extends THREE.Object3D {
constructor(label) {
super();
this.label = label;
}
copy(source, recursive = true) {
super.copy(source, recursive);
this.label = source.label;
return this;
}
}
custom propertyはThree.jsが自動で理解しないため、必要に応じてcopy / clone設計を追加します。
Materialを独立させる
cloneごとに色・透明度・wireframeなどを変える場合はMaterialもcloneします。
const clone = original.clone();
clone.material = original.material.clone();
clone.material.color.set(0xf97316);
scene.add(clone);
元記事のようにmaterial.color = new THREE.Color(...)と置き換えるより、Color instanceのset()を使うとproperty型を維持できます。色のcopy・色空間はThree.js Materialの色設定で詳しく解説しています。
Material種類・PBR property・透明度の違いはThree.js Materialの種類を参照してください。
Material配列を複製する
Geometry groupごとに複数Materialを使うMeshではmesh.materialが配列です。
const clone = original.clone();
clone.material = Array.isArray(original.material)
? original.material.map((material) => material.clone())
: original.material.clone();
Mesh.clone()がMaterial array自体を別配列にしても、配列内のMaterial instanceは共有されます。個別変更するなら各Materialをcloneします。
Geometryを独立させる
頂点・normal・uvなどをcloneだけ別に変更する場合はGeometryもcloneします。
const clone = original.clone();
clone.geometry = original.geometry.clone();
const position = clone.geometry.getAttribute("position");
position.setY(0, position.getY(0) + 0.5);
position.needsUpdate = true;
clone.geometry.computeVertexNormals();
Geometryを変更しないclone同士は、同じGeometryを共有するほうがmemory効率に優れます。Objectの大きさ・向き・位置だけ変えるならMeshのscale・rotation・positionを使います。
BufferGeometryの頂点変更、normal、bounding volume、disposeはThree.js Geometryの変更方法で解説しています。
Textureを独立させる
Material.clone()を呼んでも、map・normalMap・roughnessMapなどのTextureは通常共有されます。
Textureのoffset・repeat・rotationをcloneだけ変える場合は、対象Textureもcloneします。
const clone = original.clone();
clone.material = original.material.clone();
if (original.material.map) {
clone.material.map = original.material.map.clone();
clone.material.map.offset.set(0.5, 0);
clone.material.map.needsUpdate = true;
}
Texture cloneはTexture objectのsampling・transform設定を独立させる用途です。元画像などsource dataまで別copyになるとは限らないため、画像data自体を変更する要件は別途設計します。
Textureを変えないなら共有したほうがGPU memoryとload costを抑えられます。
Object全体を独立resourceにするhelper
通常Mesh・Group向けに、cloneした階層のMaterialとGeometryを独立させる例です。
function cloneWithIndependentResources(source) {
const clone = source.clone(true);
clone.traverse((object) => {
if (!object.isMesh) return;
object.geometry = object.geometry.clone();
object.material = Array.isArray(object.material)
? object.material.map((material) => material.clone())
: object.material.clone();
});
return clone;
}
このhelperはTextureまでcloneしません。また、すべてを独立させるとmemory使用量が増えます。「変更するresourceだけcloneする」のが基本です。
SkinnedMeshを含むmodelにはこの単純helperを使わず、SkeletonUtilsを使います。
glTFの通常modelを複製する
GLTFLoaderで読み込んだscene rootもObject3Dです。skin・boneを含まないstatic modelならsceneをcloneできます。
loader.load("/models/tree.glb", (gltf) => {
const treeA = gltf.scene;
const treeB = gltf.scene.clone(true);
treeA.position.x = -2;
treeB.position.x = 2;
scene.add(treeA, treeB);
});
static glTFでもGeometry・Materialは共有されます。見た目を共通にしたい大量のpropsには効率的ですが、片方だけMaterialを変えるなら対象MeshのMaterialをcloneします。
SkinnedMeshはSkeletonUtils.clone()を使う
bone・skeletonを含むcharacterはObject3D.clone()だけではbone associationが正しく独立しない場合があります。Three.js addonのSkeletonUtilsを使います。
import * as SkeletonUtils from "three/addons/utils/SkeletonUtils.js";
loader.load("/models/character.glb", (gltf) => {
const characterA = SkeletonUtils.clone(gltf.scene);
const characterB = SkeletonUtils.clone(gltf.scene);
characterA.position.x = -1.5;
characterB.position.x = 1.5;
scene.add(characterA, characterB);
});
SkeletonUtils.clone()はSkinnedMeshとboneを正しく対応させてcloneします。GeometryとMaterialなどはreferenceで再利用されます。
渡すsourceのdescendantにSkinnedMeshと必要なboneの両方が含まれる必要があります。個別SkinnedMeshだけではなく、通常はgltf.sceneなど共通rootを渡します。
cloneごとにanimationを再生する
AnimationClipは共有できますが、再生時刻・weight・loopなどのstateはcloneごとにAnimationMixerを作ります。
const characterA = SkeletonUtils.clone(gltf.scene);
const characterB = SkeletonUtils.clone(gltf.scene);
const clip = gltf.animations[0];
const mixerA = new THREE.AnimationMixer(characterA);
const mixerB = new THREE.AnimationMixer(characterB);
mixerA.clipAction(clip).play();
mixerB.clipAction(clip).play();
function update(delta) {
mixerA.update(delta);
mixerB.update(delta * 0.8);
}
同じclipを使いながら、cloneごとにspeedや開始時刻を変えられます。page破棄時はaction停止、rootのuncache、resource ownershipを整理します。
userDataとcustom stateの注意
Object3DのuserDataはapplication metadataを置くobjectですが、function referenceを入れないことが公式docsで推奨されています。functionはclone対象になりません。
mesh.userData = {
itemId: "item-42",
category: "furniture",
};
DOM event listener、application store、physics body、audio nodeなどThree.js外部のstateはObject3D.clone()だけでは複製されません。clone作成後のfactory処理で紐付けます。
大量複製はInstancedMeshを検討する
同じGeometry・Materialを数百・数千個表示する場合、Meshを大量cloneするとdraw callが増えます。InstancedMeshは1つのGeometry・Materialを使い、instanceごとのtransformで描画します。
const count = 500;
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshStandardMaterial({ color: 0x38bdf8 });
const instances = new THREE.InstancedMesh(geometry, material, count);
const helper = new THREE.Object3D();
for (let i = 0; i < count; i += 1) {
helper.position.set(
(Math.random() - 0.5) * 40,
(Math.random() - 0.5) * 20,
(Math.random() - 0.5) * 40,
);
helper.rotation.set(0, Math.random() * Math.PI, 0);
helper.updateMatrix();
instances.setMatrixAt(i, helper.matrix);
}
instances.instanceMatrix.needsUpdate = true;
scene.add(instances);
InstancedMeshはdraw callを減らせますが、instanceごとに異なるGeometry・Material・複雑なchild hierarchyは持てません。独立objectとしてraycast・animation・lifecycleを管理したい場合は通常cloneとのtradeoffを確認します。
多数Meshをrandom配置する基本はThree.jsでMeshをランダム配置する方法も参照してください。
instanceごとに色を変える
setColorAt()でinstance colorを設定できます。
const color = new THREE.Color();
for (let i = 0; i < count; i += 1) {
color.setHSL(i / count, 0.8, 0.5);
instances.setColorAt(i, color);
}
instances.instanceColor.needsUpdate = true;
base Material colorとinstance colorは最終表示へ組み合わされるため、元色を保つならbase colorをwhiteにするなど確認します。
cloneとdisposeの所有関係
Mesh.clone()がGeometry・Materialを共有する場合、1つのcloneを削除しただけで共有resourceをdisposeしてはいけません。
scene.remove(clone);
// originalが同じresourceを使っている間は実行しない
// clone.geometry.dispose();
// clone.material.dispose();
Three.jsはSceneからremoveしてもGeometry・Material・Textureを自動disposeしません。applicationがresourceのownerと利用数を管理します。
| resource | disposeする時点 |
|---|---|
| shared Geometry | 利用する全Meshが不要になった後 |
| independent Geometry | 所有cloneが不要になった後 |
| shared Material / Texture | 利用する全objectが不要になった後 |
| Skeleton | 共有有無を確認し、最後のSkinnedMeshが不要になった後 |
dispose済みresourceを別cloneが使い続けると、次のrenderで再生成によるcostや意図しないlifecycleになります。
複製方法の選び方
| 要件 | 方法 |
|---|---|
| 位置・回転だけ別にする | mesh.clone()でresource共有 |
| 色・opacityを別にする | Mesh clone + Material clone |
| 頂点・uvを別にする | Mesh clone + Geometry clone |
| Texture transformを別にする | Material clone + Texture clone |
| Group階層を複製する | group.clone(true) |
| bone付きglTF | SkeletonUtils.clone() |
| 同形状を大量表示 | InstancedMesh |
cloneが動かないときの確認項目
元objectまで色が変わる
Materialを共有しています。clone.materialをcloneし、Material arrayなら各要素もcloneします。
元objectの頂点まで変わる
Geometryを共有しています。頂点を個別変更する前にclone.geometryへGeometry cloneを割り当てます。
cloneをaddしたら元から消えた
同じObject3D referenceを別parentへaddしています。clone()で新しいObject3Dを作ったか確認します。
glTF characterのboneが崩れる
SkinnedMeshをObject3D.clone()だけで複製している、またはSkeletonUtils.clone()へboneを含まないsubtreeを渡している可能性があります。SkinnedMeshとboneを含むgltf.sceneなどのrootを渡します。
clone削除後に別objectの表示が不安定
共有Geometry・Material・Textureをdisposeしていないか確認します。Scene graphのremoveとGPU resourceのdisposeは別操作です。
大量cloneで重い
renderer.infoでdraw call・Geometry・Textureを確認し、同じGeometry・MaterialならInstancedMeshを検討します。すべてのcloneでMaterial・Geometryを独立させるとmemoryも増えます。
clone実装チェックリスト
- 代入ではなくclone()で新しいObject3Dを作った
- childrenが必要ならrecursive=trueを使った
- Geometry・Materialがdefaultで共有されると理解した
- 変更するresourceだけcloneした
- Material arrayの各Materialも必要に応じてcloneした
- Texture objectとsource image dataの独立範囲を確認した
- SkinnedMeshはSkeletonUtils.clone()を使った
- animation state用にcloneごとのAnimationMixerを作った
- 大量同形状ではInstancedMeshを検討した
- shared resourceを最後の利用者より前にdisposeしていない
- custom state・event・physicsは別途factoryで設定した
まとめ
Three.jsのObject3D.clone()は、Mesh・Group・子階層を新しいscene graph nodeとして複製します。ただし、MeshのGeometry・Materialはdefaultで共有されるため、完全なdeep copyではありません。
見た目だけ同じで位置を変えるならresource共有が効率的です。色を変えるならMaterial、頂点を変えるならGeometry、Texture transformを変えるならTextureも必要な範囲だけcloneします。
bone付きglTFはSkeletonUtils.clone()、同じ形を大量表示する場合はInstancedMeshを使い、disposeはresourceの最後の利用者がなくなった時点で行ってください。