Three.jsのSVGLoader:平面の画像を3Dメッシュにする
SVGのパスをThree.jsへ読み込み、実際の穴を保持し、上下の向きと押し出しメッシュのサイズを調整します。再現できる小さなブラウザー例で確認します。
目次
平面のSVG画像をThree.jsのメッシュにするには、SVGを読み込み、パスを形状へ変換し、その形状をExtrudeGeometryへ渡します。効果を加える前に、穴、上下の向き、寸法を確認します。輪郭が分かる結果でも、内側の穴が埋まったり、角の上下が逆になったりする場合があります。
この手順では、四角い穴が1つあり、右上の角を切り落とした独自の手書きバッジを使います。非対称なため、向きの誤りが見えます。ブラウザーの形状の例であり、PerfectVectorの変換結果や製造用のテストではありません。
厚みを付けたい形状から始める
最初は、閉じた塗りの輪郭を持つ小さなSVGを使います。badge.svgとして保存します。
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 240 180">
<path fill="#e64b35" fill-rule="evenodd"
d="M20 20H160L220 80V160H20Z M65 60H115V110H65Z"/>
</svg>1つのpath要素内に、外側の境界と内側の四角という2つの閉じたサブパスがあります。evenoddの塗り規則が内側を穴にします。別の白い矩形では、平面画像の面を覆うだけで、同じ空の領域を表しません。
画像は240 × 180のviewBox内で、200 × 140座標単位を占めます。メッシュのサイズを決めるときは、この違いを意識します。余白はSVGビューポートの一部であり、押し出した輪郭の一部ではありません。
編集可能な元ベクターがあれば、それを使います。PNGやJPGしかない場合は、PerfectVectorの画像をベクターに変換する手順でSVGパスの画像を準備できます。Three.jsへ読み込む前に、プレビューとダウンロードした輪郭、特に抜きの部分を確認します。ベクター化は2D画像を提供し、以下のコードが厚み、マテリアル、照明、配置を提供します。
SVGを読み込み、メッシュを描画する
この例はThree.js 0.186.0でテストしました。コアライブラリとアドオンは同じバージョンにします。パッケージインポートを使うブラウザープロジェクトではthree@0.186.0をインストールし、HTTPでファイルを配信して、コンテナーを作ります。
<div id="viewer"></div>
<script type="module" src="./main.js"></script>ビルドツールはthreeのインポートを解決する必要があります。ビルドツールなしの通常のブラウザーページでは、同じインストール済みパッケージのファイルを使い、モジュールスクリプトの前に次のimport mapを置きます。
<script type="importmap">
{
"imports": {
"three": "./node_modules/three/build/three.module.js",
"three/addons/": "./node_modules/three/examples/jsm/"
}
}
</script>HTMLページの横にbadge.svgを置きます。以下の相対読み込みURLは、そのページのURLを基準に解決します。次をmain.jsとして保存します。
import * as THREE from 'three';
import { SVGLoader } from 'three/addons/loaders/SVGLoader.js';
const data = await new SVGLoader().loadAsync('./badge.svg');
const artwork = new THREE.Group();
for (const path of data.paths) {
if (path.userData.style.fill === 'none') continue;
for (const shape of path.toShapes()) {
const geometry = new THREE.ExtrudeGeometry(shape, {
depth: 20, bevelEnabled: false, steps: 1
});
const material = new THREE.MeshStandardMaterial({
color: path.color, roughness: 0.65, metalness: 0
});
artwork.add(new THREE.Mesh(geometry, material));
}
}
if (!artwork.children.length) throw new Error('No filled shapes found');
// SVG y grows down; this scene uses y up.
artwork.scale.y = -1;
const bounds = new THREE.Box3().setFromObject(artwork);
const size = bounds.getSize(new THREE.Vector3());
const center = bounds.getCenter(new THREE.Vector3());
const scale = 4 / size.x;
artwork.scale.multiplyScalar(scale);
artwork.position.copy(center).multiplyScalar(-scale);
const scene = new THREE.Scene();
scene.background = new THREE.Color('#f1f3f5');
scene.add(artwork);
scene.add(new THREE.HemisphereLight(0xffffff, 0x596477, 2));
const light = new THREE.DirectionalLight(0xffffff, 3);
light.position.set(-3, 5, 7);
scene.add(light);
const camera = new THREE.PerspectiveCamera(35, 800 / 600, 0.1, 100);
camera.position.set(4, 3, 9);
camera.lookAt(0, 0, 0);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(800, 600);
document.querySelector('#viewer').append(renderer.domElement);
renderer.render(scene, camera);SVGLoaderは、別途インポートするアドオンです。loadAsync()はファイルを読みます。SVGマークアップを文字列としてすでに持つ場合は、parse(svgText)を使えます。
r186ではpath.toShapes()を使います。古い例はSVGLoader.createShapes(path)を呼ぶことがよくあります。このヘルパーはr185で非推奨となり、テストしたバージョンではtoShapes()へ委譲します。異なるチュートリアルのコードを混ぜる前に、インストール済みのリリースを確認します。
ループは意図してfill="none"を除外します。この例の単色の塗りを扱うもので、完全なSVGレンダラーではありません。グラデーション、マスク、埋め込み画像、ストローク、不透明度、重なる色レイヤーは、平面の見た目が同じ3D形状になると想定せず、それぞれ判断が必要です。

測定と中央配置の前に向きを設定する
SVGの垂直座標は下へ増えます。このシーンは正のyが上向きで画像を見るため、artwork.scale.y = -1で期待する向きに戻します。右上にあるはずの切り落とした角が確認点です。
この符号変更は、この座標設定に対応するものです。カメラや親オブジェクトを回した後も見た目がおかしいからといって、2回目の反転を加えないでください。まず、元データの既知の非対称な特徴を描画したシーンと比較します。
次の行では、反転後のグループを測定し、等倍率を計算して、拡大縮小した中心を原点へ移動します。Box3.setFromObject()は、子オブジェクトと変換も含めた境界を測定します。ここでは、測定時にグループの親に変換はなく、画像も回転していません。
元の押し出しは200 × 140 × 20です。幅を4に設定すると、倍率は4 / 200、つまり0.02です。3軸すべてへ適用するため、結果の境界は4 × 2.8 × 0.4になります。コンストラクターの厚み20は、グループの拡大縮小後も20のままではありません。
これらはシーンの単位です。1単位が1ミリメートルだとは主張しません。アプリに特定の実寸解釈が必要なら、明示的に定義し、対象の手順で検証します。
表面を装飾する前に穴を確認する
テストでは、読み込んだパス1本、形状1つ、穴1つが得られました。四角い穴と塗りのある部分へそれぞれレイを投射しました。穴にはメッシュのヒットがなく、塗りのある部分にはヒットがありました。切り落とした右上の角と、塗りのある右下の角へのレイで、斜めの視点とは別に上下の向きも確認しました。
自分のシーンでは、より単純な目視確認から始められます。対照的な背景で、正面と斜めから面を見ます。穴には背景が見え、カメラを動かすと側壁が見えるはずです。白い面が領域を覆っているなら、別の結果です。
内側の穴が埋まる場合は、その形だけを取り出します。元データがサブパス、別の塗りオブジェクト、クリップパス、マスクのどれを使うか確認します。マテリアルの色で問題を隠そうとしないでください。SVGの穴が埋まる問題のガイドは元ファイルの違いを説明し、押し出しの診断は元画像の輪郭が壊れている場合の修復を扱います。
単純な押し出しが動いてから厚みの効果を加える
この例では、初期寸法を確認しやすいようベベルを無効にしています。ExtrudeGeometryのオプションは、厚み、ベベルサイズ、ベベルの厚み、曲線のサンプリングを別々に制御します。ベベルを有効にしたら、小さな穴と境界を再確認します。装飾の縁は、測定した形状を変えます。
曲線の画像では、アプリで使うサイズとカメラ距離でメッシュの輪郭を確認します。角張りが問題となる場合だけ曲線サンプリングを増やします。入力の不要なノードを減らすのは別の編集作業です。どちらも、元データにない細部は復元しません。
ストロークだけの描画にも、別の判断が必要です。ローダーはストローク形状用のpointsToStroke()を提供しますが、上の塗り形状のループは意図してそのパスを除外します。見える線を押し出したリボンにするなら、まずリボンを定義する閉じた輪郭を決めます。シーン内で線のままにするなら、線やストロークとして表現します。
Reactアプリでは、ブラウザー専用の読み込みとレンダラーの処理をクライアントで実行し、ビューが削除されたら、所有する形状、マテリアル、レンダラーを破棄します。この小さな例は固定サイズで1回描画します。製品用ビューアーには、リサイズ処理と独自のリソース管理も必要です。
元データと対象環境の確認を分ける
ブラウザーのメッシュは、対話型バッジ、ロゴプレビュー、シーンの装飾に役立ちます。結果が水密で、部品として適切な寸法であることは証明しません。製作が目的なら、別の3Dプリント用の画像からSVGへの手順を使い、そこで読み込んだモデルを確認します。
画面向けの手順では、編集可能な元SVG、派生メッシュ、シーンでの表示の3つを分けます。悪い結果がパス形状、メッシュ構築、カメラと照明のどこから来たか判断しやすくなります。読み込み前に画像の修復が必要な場合は、他のSVGワークフローガイドが元データ側を扱っています。
よくある質問
Three.jsでSVGの上下が逆になるのはなぜですか? SVGの垂直座標は下へ増えます。正のyを上向きとし、画像を正面から見るシーンでは、画像グループのy倍率を反転すると元の向きに戻せます。別の反転や回転を追加する前に、非対称な特徴を確認します。
createShapesとtoShapesのどちらを使うべきですか? この例はThree.js 0.186.0でpath.toShapes()を使います。SVGLoader.createShapes()はr185で非推奨となりました。コアとアドオンを同じリリースにし、インストール済みバージョンで文書化されたAPIを使います。
SVGLoaderはSVG内のPNGをメッシュにしますか? 埋め込みラスター画像は、この例が使う塗りの輪郭を提供しません。適したパス画像を先に用意するか、見た目の保持が目的なら画像をテクスチャとして使います。
押し出しはSVGのストローク幅を保持しますか? この例は塗り形状を押し出し、fillがnoneのパスを除外します。ストローク形状には別の手順が必要です。太く描画された線が、自動で押し出したリボンの閉じた輪郭になるわけではありません。
参考資料
- Three.js — SVGLoader — アドオンのインポート、解析と読み込み、ストローク用ヘルパー、createShapesの非推奨を説明しています。
- Three.js — ShapePath — toShapesによるパス集合から形状への変換を説明しています。
- Three.js — ExtrudeGeometry — 形状の押し出し、厚み、ベベル、曲線サンプリングのオプションを定義しています。
- Three.js — Box3 — 変換済みオブジェクトの境界測定と、サイズ・中心の取得を説明しています。
ラスターのエンブレムしかない場合は、SVGの候補を準備し、塗りの輪郭と穴を確認してから、完全なビューアーを作る前に、この小さなシーンでファイルをテストしてください。

