🎃
Three.js + TypeScriptで断面表示(ローカルクリッピング)
内容
- これはTypeScriptからThree.jsを使うサンプルである。
- 3Dオブジェクトの一部を非表示にするクリッピングを試す。
- 断面を描画する場合と、しない場合。
プロジェクトの作り方は、以下の記事と同様
断面を描画しない場合
オブジェクトの「ある平面より上の部分を非表示」のような表示は、とても簡単にできる。本質的な部分を取り出すと以下のようになる。
renderer.localClippingEnabled = true; //重要
const plane = new THREE.Plane(法線ベクトル, 原点からの距離);
const mesh = new THREE.Mesh(
new THREE.SphereGeometry(),// あるいは他のジオメトリ
new THREE.MeshNormalMaterial({// あるいは他のマテリアル
clippingPlanes:[plane], // クリッピングする平面を指定
side:THREE.DoubleSide, // クリッピングしたら大概裏側が見えるはず
}),
);
-
clippingPlanes
に複数の平面を指定すると、領域の論理積(つまり共通部分)が表示される。 -
clipShadows
をtrueにすると、影も正しく表示される。 -
clipIntersection
をtrueにすると、論理積ではなく論理和が表示される。
clippingPlanesを作るヘルパー関数
箱型に切り抜くなら、以下のような関数を作っておくと便利かもしれない。
export function createClippingBox({maxX,minX,maxY,minY,maxZ,minZ}:{
maxX?:number,minX?:number,maxY?:number,minY?:number,maxZ?:number,minZ?:number
}){
const planes:THREE.Plane[] = [];
if(maxX)planes.push(new THREE.Plane(new THREE.Vector3(-1, 0, 0), maxX));
if(minX)planes.push(new THREE.Plane(new THREE.Vector3(1, 0, 0), -minX));
if(maxY)planes.push(new THREE.Plane(new THREE.Vector3(0, -1, 0), maxY));
if(minY)planes.push(new THREE.Plane(new THREE.Vector3(0, 1, 0), -minY));
if(maxZ)planes.push(new THREE.Plane(new THREE.Vector3(0, 0, -1), maxZ));
if(minZ)planes.push(new THREE.Plane(new THREE.Vector3(0, 0, 1), -minZ));
return planes;
}
断面を描画する場合
公式のサンプルコードがあるので、それを参考にする。
clippingPlanes
でオブジェクトを切断した後、断面の形状に切り抜いた平面を描画するのだが、かなり複雑なことをしなくてはいけない[1]。
StencilBufferを使って、断面の形状を描画するので、renderer
を作るときに、stencil
をtrueにする必要がある。
const renderer = new THREE.WebGLRenderer({stencil: true});
かなりごちゃごちゃしてしまうので、別ファイルclippingStencil.ts
にまとめておくことにする。
clippingStencil.ts
import * as THREE from 'three';
export function setClipping(
scene:THREE.Scene,
clippingPlanes:THREE.Plane[],
clippingTargetMesh:THREE.Mesh,
crossSectionMaterial:THREE.Material
):()=>void {
const planeObjects:THREE.Mesh[] = [];
const object = new THREE.Group();
scene.add(object);
const planeGeom = new THREE.PlaneGeometry(10, 10);
for(let i=0; i<clippingPlanes.length; i++){
const plane = clippingPlanes[i];
const stencilGroup = createPlaneStencilGroup( clippingTargetMesh.geometry, plane, i+1);
const planeMat = crossSectionMaterial.clone();
planeMat.clippingPlanes = clippingPlanes.filter( p => p !== plane );
planeMat.stencilWrite = true;
planeMat.stencilFunc = THREE.NotEqualStencilFunc;
planeMat.stencilFail = THREE.ReplaceStencilOp;
planeMat.stencilZFail = THREE.ReplaceStencilOp;
planeMat.stencilZPass = THREE.ReplaceStencilOp;
const po = new THREE.Mesh( planeGeom, planeMat);
po.onAfterRender = ( renderer ) =>renderer.clearStencil();
po.renderOrder = i+1.1;
object.add( stencilGroup );
object.add( po );
planeObjects.push( po );
}
const update = ()=>{
for (let i = 0; i < clippingPlanes.length; i ++ ) {
const plane = clippingPlanes[ i ];
const po = planeObjects[ i ];
plane.coplanarPoint( po.position );
po.lookAt(
po.position.x - plane.normal.x,
po.position.y - plane.normal.y,
po.position.z - plane.normal.z,
);
}
};
update();
return update;
}
function createPlaneStencilGroup(
geometry:THREE.BufferGeometry,
clippingPlane:THREE.Plane,
renderOrder:number
) {
const group = new THREE.Group();
const baseMat = new THREE.MeshBasicMaterial();
baseMat.depthWrite = false;
baseMat.depthTest = false;
baseMat.colorWrite = false;
baseMat.stencilWrite = true;
baseMat.stencilFunc = THREE.AlwaysStencilFunc;
// back faces
const mat0 = baseMat.clone();
mat0.side = THREE.BackSide;
mat0.clippingPlanes = [ clippingPlane ];
mat0.stencilFail = THREE.IncrementWrapStencilOp;
mat0.stencilZFail = THREE.IncrementWrapStencilOp;
mat0.stencilZPass = THREE.IncrementWrapStencilOp;
const mesh0 = new THREE.Mesh( geometry, mat0 );
mesh0.renderOrder = renderOrder;
group.add( mesh0 );
// front faces
const mat1 = baseMat.clone();
mat1.side = THREE.FrontSide;
mat1.clippingPlanes = [ clippingPlane ];
mat1.stencilFail = THREE.DecrementWrapStencilOp;
mat1.stencilZFail = THREE.DecrementWrapStencilOp;
mat1.stencilZPass = THREE.DecrementWrapStencilOp;
const mesh1 = new THREE.Mesh( geometry, mat1 );
mesh1.renderOrder = renderOrder;
group.add( mesh1 );
return group;
}
setClipping
関数を呼ぶと、()=>void
型の関数が返ってくるので、それを毎フレーム呼ぶことで、断面の位置を更新できる。
clippingPlanes
を適当に回転させながら、アニメーション表示してみた。
-
よくわからないこともあるけど、とりあえず動く。 ↩︎
Discussion