はじめに
この記事はProcessing Advent Calendar 2025の24日目の記事です。
Blenderにシェイプキーアニメーションというのがあって、シェイプキーを用意してアニメーションを登録することができます。複数でもできます。そういうのをp5.jsでやろうと思います。p5.Geometryを使います。Blenderも使います。
二本立てで行きます。まず自分でこしらえた、複数のアニメーションを有するモデルに対するシェイプキーアニメーションの実行のコードを書きます。そのあとで応用として、Threeにあるアセットを改変した動物アニメーションを動かしてみようと思います。
gltfについて
たとえばメッシュ作って表示するだけの場合、こういう記事を以前書きました。
embedのgltfという形式があって、要するにオールインワンの3D版JPEGですね。オールインワンならglbもそうなんですが(小さいんですが)、glbと違ってJSONで完結するので解析が楽なんですよね。なので愛用しています。
コード全文
とりあえずコードです。なおp5.jsは最新の2.1.2です。OpenProcessingのリンク:
/*
output版です
p5は2.1.2でやります
カメラは最後に適当に用意します
あっちの解析機構を移植してp5.Geometryをフレーム数分だけ作る形に書き換えましょう
複数アニメです
animalはすべて単独アニメなので...
法線はあらかじめ計算した方がいいかと思うけれど難しい
変形に弱いので頂点色が向いてると思う
*/
async function setup() {
createCanvas(400, 400, WEBGL);
smooth(); // for Firefox.
// パッチ当てるか
// このパッチを当てるといいらしいです
window.addEventListener("pointercancel", (e)=>{
p5.instance._activePointers.delete(e.pointerId);
p5.instance._updatePointerCoords(e);
});
const src = await loadJSON("https://inaridarkfox4231.github.io/resources/shapeKeyAnimation.gltf");
const srcColored = await loadJSON("https://inaridarkfox4231.github.io/resources/shapeKeyAnimationColored.gltf");
const gl = drawingContext;
const gltf = new MonoMeshShapekeyGltfColored(gl, src);
const gltfColored = new MonoMeshShapekeyGltfColored(gl, srcColored);
// 去年も使ったやつ
// blenderと同じ見た目にするため、まずy軸正方向を上に取る
camera(320,320,320, 0,160,0, 0,1,0);
const eyeDist = dist(320,320,320, 0,160,0);
// そのうえでfrustumモードを使い、上下を逆転させ、射影行列の-1を殺す。
const nearPlaneHeightHalf = eyeDist*tan(PI/6)*0.1;
const nearPlaneWidthHalf = nearPlaneHeightHalf*width/height;
// ここの第3,4引数。
frustum(-nearPlaneWidthHalf, nearPlaneWidthHalf, nearPlaneHeightHalf, -nearPlaneHeightHalf, 0.1*eyeDist, 10*eyeDist);
const config = {
index:0,
colored:false
};
const gui = new lil.GUI();
const actionParameter = {};
for(let i=0; i<gltf.animations.length; i++){
actionParameter[`action${i}`] = i;
}
gui.add(config, "index", actionParameter).name("action");
gui.add(config, "colored");
draw = () => {
const animation = (config.colored ? gltfColored.animations[config.index] : gltf.animations[config.index]);
background(200);
orbitControl(1,-1,1);
scale(40);
const t = frameCount*TAU/120;
directionalLight(128,128,128,cos(t),-2,sin(t));
ambientLight(64);
specularMaterial(32);
if(config.colored){ noLights(); }
fill(128,192,255);
model(animation.geoms[frameCount%animation.frameNum]);
}
}
// 仮定:メッシュは単独、シェイプキーアニメーションは...複数がいいですね...
// 色はあったら追加する形で
// animations[0].samplers[0].outputです。ここにウェイトがフレームごとに入ってる。
// 0フレームのウェイト列、1フレームのウェイト列、...って感じ。
class MonoMeshShapekeyGltfColored{
constructor(gl, gltfjson){
this.gl = gl;
this.gltf = gltfjson;
this.buffers = [];
this.encodeBuffers();
this.bufferViews = [];
this.encodeBufferViews();
// vaoは諸事情により使いません。代わりにp5.Geometry~~~
this.animations = [];
this.createGltf();
}
encodeBuffers(){
const {buffers} = this.gltf;
for(let i=0; i<buffers.length; i++){
const buffer = buffers[i];
const {byteLength, uri} = buffer;
const ab = new ArrayBuffer(byteLength);
const ua = new Uint8Array(ab);
const bin = uri.split(',')[1];
const byteString = atob(bin);
for (let i = 0; i < byteString.length; i++) {
ua[i] = byteString.charCodeAt(i);
}
this.buffers.push(ab);
}
}
encodeBufferViews(){
// DataViewはoffsetとlengthから作れるのでそうする
const {bufferViews} = this.gltf;
for(let i=0; i<bufferViews.length; i++){
const bufferView = bufferViews[i];
const {buffer, byteOffset, byteLength} = bufferView;
// DataView:https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/DataView
const dw = new DataView(this.buffers[buffer], byteOffset, byteLength);
this.bufferViews.push(dw);
}
}
createGltf(){
// geoms: p5.Geometryの配列
// frameNum: フレーム数
// step1: Float32でv.n, Uint16でfを用意する。色は無しで。Coloredもついてないし。
const primitive = this.gltf.meshes[0].primitives[0]
const {POSITION, NORMAL} = primitive.attributes;
// targetsあとで使うんですよね
const {indices, targets} = primitive;
const weightNum = targets.length;
// ベースのv,n,f
const v = toFloat32(this.bufferViews[this.gltf.accessors[POSITION].bufferView]);
const n = toFloat32(this.bufferViews[this.gltf.accessors[NORMAL].bufferView]);
const f = toUint16(this.bufferViews[this.gltf.accessors[indices].bufferView]);
// step2: targetsの情報を元に、ウェイトごとのvとnを用意して配列にぶち込む
const vArray = [];
const nArray = [];
for(let k=0; k<weightNum; k++){
vArray.push(toFloat32(this.bufferViews[this.gltf.accessors[targets[k].POSITION].bufferView]));
nArray.push(toFloat32(this.bufferViews[this.gltf.accessors[targets[k].NORMAL].bufferView]));
}
const VERTEX_NUM = vArray[0].length;
// 色があったら用意する
const c = [];
if(primitive.attributes.COLOR_0 !== undefined){
const cc = toUint16(this.bufferViews[this.gltf.accessors[primitive.attributes.COLOR_0].bufferView]);
c.push(...cc);
for(let i=0; i<c.length; i++){
c[i] /= 65536;
}
}
// step3: これらにもともとのvとnを足す
// 間違い。ここで足すな!!
/*
for(let k=0; k<weightNum; k++){
for(let i=0; i<VERTEX_NUM; i++){
vArray[k][i] += v[i];
nArray[k][i] += n[i];
}
}
*/
// 下準備はここまで
const animations = this.gltf.animations;
const animationNum = animations.length;
// ShapeKeyAnimationをあるだけぶちこむ
for(let i=0; i<animationNum; i++){
const SAMPLER_OUTPUT = animations[i].samplers[0].output;
// ここにウェイト数xフレーム数の数の配列が入ってる
const output = toFloat32(this.bufferViews[this.gltf.accessors[SAMPLER_OUTPUT].bufferView]);
//console.log(output);
const frameNum = Math.floor(output.length/weightNum);
const result = {}; // geomsとframesを入れる...
result.frameNum = frameNum;
// geomを作る
const geoms = [];
for(let k=0; k<frameNum; k++){
const geom = new p5.Geometry();
const vv = new Array(VERTEX_NUM);
const nn = new Array(VERTEX_NUM);
vv.fill(0);
nn.fill(0);
for(let l=0; l<weightNum; l++){
const w = output[k*weightNum + l];
if(w===0){continue;}
for(let m=0; m<VERTEX_NUM; m++){
vv[m] += w * vArray[l][m];
nn[m] += w * nArray[l][m];
}
}
// そうです。ここで足します。
for(let m=0; m<VERTEX_NUM; m++){
vv[m] += v[m];
nn[m] += n[m];
}
// p5.Geometryに翻訳
for(let m=0; m<VERTEX_NUM; m+=3){
geom.vertices.push(createVector(vv[m], vv[m+1], vv[m+2]));
geom.vertexNormals.push(createVector(nn[m], nn[m+1], nn[m+2]).normalize());
}
for(let fi=0; fi<f.length; fi+=3){
geom.faces.push([f[fi], f[fi+1], f[fi+2]]);
}
// 色がある場合はそれも追加
if(c.length > 0){
for(let m=0; m<c.length; m+=4){
geom.vertexColors.push(c[m], c[m+1], c[m+2], c[m+3]);
}
}
geoms.push(geom);
}
result.geoms = geoms;
this.animations.push(result);
}
}
}
// p5.Geometry用の補助関数
function toFloat32(dw){
const result = [];
for(let k=0; k<dw.byteLength; k+=4){
result.push(dw.getFloat32(k, true));
}
return result;
}
// これでやって、そんで65536で割ればいいと思う
function toUint16(dw){
const result = [];
for(let k=0; k<dw.byteLength; k+=2){
result.push(dw.getUint16(k, true));
}
return result;
}
順を追って説明します。
シェイプキーアニメーションとは
Blenderのアニメーションの手法の一つで、要するに頂点の配置を補間して楽にアニメーションを作ろうというわけですね。この辺が参考になります。
自分がどうやって作ってるかについても簡単に説明します(ざっくり)。まず、ここに書いてあるようにキーを追加します。今回は4つ追加しました。それで、キーを選択した状態で編集モードに行って変形するわけですね。
こんな風に(図はKey2)。それでアニメーションは、まずAnimationのTimeline,まあこれがデフォルトですが、その一つ上のDopeSheetを選んで、その中のShapeKeyEditorですね。これを選ぶわけです。
それで次に、中央の+Newを押して作成し適当に名前を付けます。このときフェイクユーザー(盾マーク)にチェックを入れて保存しても失われないようにしましょう。
あとはKeyのところ、割合、これをいじりながらキーを打っていきます。この辺は上の記事でも紹介されていますね。それでアニメーションができました。次です。他のアニメーションを作るにはまず盾の右の✖ボタンを押して消します。消して、消すと+Newが復活するんでそれをまた押して、名前を付けて、キーを打って...これで順繰りに作っていきます。合計4つ、作りました。
それではこれをgltfで出力しましょう。
gltf形式でシェイプキー込みのデータを出力する
ここは難しいんですが、次の記事が役に立ってくれました(感謝...!):
まず、盾マークの左側にPushDownってありますね。これを、すべてのアニメーションについて1回ずつ押します。そして、Animationの5つの項目の一番下、NonLinearAnimationを押します。
そうするとさっき送ったアニメーションが並んでいます(Action0,1,2,3)。ここで長さとか変えられるらしい?(未検証)それで、この状態のままでembedのgltfを書き出すわけです。そうすると、4つのアニメーションがきちんと登録されます。たとえばTimelineなどの表示のままgltfで出すとそのアニメーションしか反映されないんですよね。困ってたので助かりました...
ここからはgltfの解析になります。
gltfを解析する
gltfにはembedのものと、そうでないのがあるんですが、ぶっちゃけ分かれてると不便なので自分はembedでやってます(データ量はembedでない方が小さくなるようです)。それを実行するのがMonoMeshShapekeyGltfColoredクラスです。コードの下半分です。
いくつか仮定して解析しやすくしています。つまり完全ではないということです。ケースによって内容が異なるため、汎用性を持たせるのは難しいからです。まず単独メッシュです。次に、そのメッシュに複数のシェイプキーアニメーションが登録されています。シェイプキー以外のキーは存在せず、純粋にシェイプキーのみの操作で作られています。そして他のタイプのアニメーションは存在しません。以上です。なお、頂点色は存在する場合のみ考慮されます。
詳しくは割愛するので雰囲気だけ伝えると、まずbase64形式のめっちゃ長い文字列が埋め込まれているんですね。数データです。これをbufferViewsという情報に基づいて部分ごと切り分けてDataView形式で出力します。おそらくどういう解析をするにせよ、ここだけは共通のはずです。
encodeBuffers(){
const {buffers} = this.gltf;
for(let i=0; i<buffers.length; i++){
const buffer = buffers[i];
const {byteLength, uri} = buffer;
const ab = new ArrayBuffer(byteLength);
const ua = new Uint8Array(ab);
const bin = uri.split(',')[1];
const byteString = atob(bin);
for (let i = 0; i < byteString.length; i++) {
ua[i] = byteString.charCodeAt(i);
}
this.buffers.push(ab);
}
}
encodeBufferViews(){
// DataViewはoffsetとlengthから作れるのでそうする
const {bufferViews} = this.gltf;
for(let i=0; i<bufferViews.length; i++){
const bufferView = bufferViews[i];
const {buffer, byteOffset, byteLength} = bufferView;
// DataView:https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/DataView
const dw = new DataView(this.buffers[buffer], byteOffset, byteLength);
this.bufferViews.push(dw);
}
}
DataView形式にこだわるのはこれが便利だからですね。たとえばこれをFloat32とかUint16とかにしたい場合、こんな感じで取り出せます。なお
WebGLはリトルエンディアン
ですから注意しましょう。なぜならDataViewはデフォルトではビッグエンディアンで解釈するからです。
// p5.Geometry用の補助関数
function toFloat32(dw){
const result = [];
for(let k=0; k<dw.byteLength; k+=4){
result.push(dw.getFloat32(k, true));
}
return result;
}
// これでやって、そんで65536で割ればいいと思う
function toUint16(dw){
const result = [];
for(let k=0; k<dw.byteLength; k+=2){
result.push(dw.getUint16(k, true));
}
return result;
}
なおDataViewをdvではなくdwと略しているのはdvだと心象が悪いからです(どうでもいい)。
こういうのを内部でp5.Geometryを作るのに使っています。
ShapeKeyAnimationの出力
アニメーションのフレーム数などもすべて計算されています。それもデータに入っています。その出力のための処理を後半で実行しています。createGltfですね。他の場合は考慮していません。割と特化した内容です。
位置と法線と面のインデックスの情報を取得します。そして、ウェイトごとの頂点や法線の情報が差分の形で入っているのでそれを取得して配列に入れます。そして、フレームごとのウェイト分布の情報が入っているので、それにより重み付き補間で出します。そうしてフレームごとにp5.Geometryを作り、毎フレーム描画することでアニメーションとします。
なお、色がある場合はCOLOR_0に入っています。複数登録できるらしい?まあ基本1つしかないのでCOLOR_0ですね。なお、これをgltfに反映させるにはノードをつないで色が付くようにしておかないといけないんで、注意してください。コンフィグのcoloredをチェックすると、頂点色になります。
これであとはアニメーションごとにジオメトリーの列が作られるんで、それぞれ再生して遊べます。お疲れ様でした。
Threeのアセットを動かしてみよう
これだけじゃ面白くないんで、Threeから拝借したFlamingo,Stork,Horse,Parrotのモデルを動かすデモを用意しました。すべてシェイプキーアニメーションです。ポーズごとにシェイプキーを設定して、補間で動かしています。まあこういうのは普通スキンメッシュでやるんでしょうが...でもちゃんと動きます。
// 出典:おそらくThreeのどこか。
async function setup() {
createCanvas(400, 400, WEBGL);
smooth(); // for Firefox.
const gl = drawingContext;
// パッチ当てるか
// このパッチを当てるといいらしいです
window.addEventListener("pointercancel", (e)=>{
p5.instance._activePointers.delete(e.pointerId);
p5.instance._updatePointerCoords(e);
});
const animations = [];
const animals = ["Flamingo", "Stork", "Horse", "Parrot"];
const visualizeScale = [2.2,2.5,1,4];
for(const animal of animals){
const url = `https://inaridarkfox4231.github.io/resources/${animal}_smooth.gltf`;
const src = await loadJSON(url);
const gltf = new MonoMeshShapekeyGltfColored(gl, src);
// ひとつしかないんで。
animations.push(gltf.animations[0]);
}
// 去年も使ったやつ
// blenderと同じ見た目にするため、まずy軸正方向を上に取る
camera(240,180,320, 0,0,0, 0,1,0);
const eyeDist = dist(240,180,320, 0,0,0);
// そのうえでfrustumモードを使い、上下を逆転させ、射影行列の-1を殺す。
const nearPlaneHeightHalf = eyeDist*tan(PI/6)*0.1;
const nearPlaneWidthHalf = nearPlaneHeightHalf*width/height;
// ここの第3,4引数。
frustum(-nearPlaneWidthHalf, nearPlaneWidthHalf, nearPlaneHeightHalf, -nearPlaneHeightHalf, 0.1*eyeDist, 10*eyeDist);
const config = {
index:0
};
const gui = new lil.GUI();
gui.add(config, "index", {"Flamingo":0, "Stork":1, "Horse":2, "Parrot":3}).name("animals");
draw = () => {
const animation = animations[config.index];
background(200);
orbitControl(1,-1,1);
const t = frameCount*TAU/240;
directionalLight(128,128,128,cos(t),-2,sin(t));
ambientLight(128);
specularMaterial(64);
fill(255);
scale(visualizeScale[config.index]);
model(animation.geoms[frameCount%animation.frameNum]);
}
}
// 仮定:メッシュは単独、シェイプキーアニメーションは...複数がいいですね...
// 色はあったら追加する形で
// animations[0].samplers[0].outputです。ここにウェイトがフレームごとに入ってる。
// 0フレームのウェイト列、1フレームのウェイト列、...って感じ。
class MonoMeshShapekeyGltfColored{
constructor(gl, gltfjson){
this.gl = gl;
this.gltf = gltfjson;
this.buffers = [];
this.encodeBuffers();
this.bufferViews = [];
this.encodeBufferViews();
// vaoは諸事情により使いません。代わりにp5.Geometry~~~
this.animations = [];
this.createGltf();
}
encodeBuffers(){
const {buffers} = this.gltf;
for(let i=0; i<buffers.length; i++){
const buffer = buffers[i];
const {byteLength, uri} = buffer;
const ab = new ArrayBuffer(byteLength);
const ua = new Uint8Array(ab);
const bin = uri.split(',')[1];
const byteString = atob(bin);
for (let i = 0; i < byteString.length; i++) {
ua[i] = byteString.charCodeAt(i);
}
this.buffers.push(ab);
}
}
encodeBufferViews(){
// DataViewはoffsetとlengthから作れるのでそうする
const {bufferViews} = this.gltf;
for(let i=0; i<bufferViews.length; i++){
const bufferView = bufferViews[i];
const {buffer, byteOffset, byteLength} = bufferView;
// DataView:https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/DataView
const dw = new DataView(this.buffers[buffer], byteOffset, byteLength);
this.bufferViews.push(dw);
}
}
createGltf(){
// geoms: p5.Geometryの配列
// frameNum: フレーム数
// step1: Float32でv.n, Uint16でfを用意する。色は無しで。Coloredもついてないし。
const primitive = this.gltf.meshes[0].primitives[0]
const {POSITION, NORMAL} = primitive.attributes;
// targetsあとで使うんですよね
const {indices, targets} = primitive;
const weightNum = targets.length;
// ベースのv,n,f
const v = toFloat32(this.bufferViews[this.gltf.accessors[POSITION].bufferView]);
const n = toFloat32(this.bufferViews[this.gltf.accessors[NORMAL].bufferView]);
const f = toUint16(this.bufferViews[this.gltf.accessors[indices].bufferView]);
// step2: targetsの情報を元に、ウェイトごとのvとnを用意して配列にぶち込む
const vArray = [];
const nArray = [];
for(let k=0; k<weightNum; k++){
vArray.push(toFloat32(this.bufferViews[this.gltf.accessors[targets[k].POSITION].bufferView]));
nArray.push(toFloat32(this.bufferViews[this.gltf.accessors[targets[k].NORMAL].bufferView]));
}
const VERTEX_NUM = vArray[0].length;
// 色があったら用意する
const c = [];
if(primitive.attributes.COLOR_0 !== undefined){
const cc = toUint16(this.bufferViews[this.gltf.accessors[primitive.attributes.COLOR_0].bufferView]);
c.push(...cc);
for(let i=0; i<c.length; i++){
c[i] /= 65536;
}
}
// step3: これらにもともとのvとnを足す
// 間違い。ここで足すな!!
/*
for(let k=0; k<weightNum; k++){
for(let i=0; i<VERTEX_NUM; i++){
vArray[k][i] += v[i];
nArray[k][i] += n[i];
}
}
*/
// 下準備はここまで
const animations = this.gltf.animations;
const animationNum = animations.length;
// ShapeKeyAnimationをあるだけぶちこむ
for(let i=0; i<animationNum; i++){
const SAMPLER_OUTPUT = animations[i].samplers[0].output;
// ここにウェイト数xフレーム数の数の配列が入ってる
const output = toFloat32(this.bufferViews[this.gltf.accessors[SAMPLER_OUTPUT].bufferView]);
//console.log(output);
const frameNum = Math.floor(output.length/weightNum);
const result = {}; // geomsとframesを入れる...
result.frameNum = frameNum;
// geomを作る
const geoms = [];
for(let k=0; k<frameNum; k++){
const geom = new p5.Geometry();
const vv = new Array(VERTEX_NUM);
const nn = new Array(VERTEX_NUM);
vv.fill(0);
nn.fill(0);
for(let l=0; l<weightNum; l++){
const w = output[k*weightNum + l];
if(w===0){continue;}
for(let m=0; m<VERTEX_NUM; m++){
vv[m] += w * vArray[l][m];
nn[m] += w * nArray[l][m];
}
}
// そうです。ここで足します。
for(let m=0; m<VERTEX_NUM; m++){
vv[m] += v[m];
nn[m] += n[m];
}
// p5.Geometryに翻訳
for(let m=0; m<VERTEX_NUM; m+=3){
geom.vertices.push(createVector(vv[m], vv[m+1], vv[m+2]));
geom.vertexNormals.push(createVector(nn[m], nn[m+1], nn[m+2]).normalize());
}
for(let fi=0; fi<f.length; fi+=3){
geom.faces.push([f[fi], f[fi+1], f[fi+2]]);
}
// 色がある場合はそれも追加
if(c.length > 0){
for(let m=0; m<c.length; m+=4){
geom.vertexColors.push(c[m], c[m+1], c[m+2], c[m+3]);
}
}
geoms.push(geom);
}
result.geoms = geoms;
result.count = f.length;
this.animations.push(result);
}
}
}
// p5.Geometry用の補助関数
function toFloat32(dw){
const result = [];
for(let k=0; k<dw.byteLength; k+=4){
result.push(dw.getFloat32(k, true));
}
return result;
}
// これでやって、そんで65536で割ればいいと思う
function toUint16(dw){
const result = [];
for(let k=0; k<dw.byteLength; k+=2){
result.push(dw.getUint16(k, true));
}
return result;
}
なおnoLightsでもよかったんですがなんかのっぺりしちゃうんでライティングを付与しました。ちなみにp5.Geometryが頂点色を反映できるようになったのはp5.jsの1.6.0です。自分が実装しました。
以下、いくつか補足します。
smooth()
現在、p5ではFirefoxなどのsafariを含まないブラウザにおいてはWebGLでantialiasが切ってあるんですね。それを回避するためにsmooth()を使っています。いずれ直してほしい...
orbitControl用のパッチ
現在、p5の最新版ではスマホなどでorbitControlを動かそうとすると、画面が小さい場合、画面外をスワイプした瞬間に内部で操作することができなくなります。それを回避するためにパッチを当てています。
// パッチ当てるか
// このパッチを当てるといいらしいです
window.addEventListener("pointercancel", (e)=>{
p5.instance._activePointers.delete(e.pointerId);
p5.instance._updatePointerCoords(e);
});
直されれば要らなくなるんですが...誰か直してください。
preload廃止
p5.jsの2系ではpreloadがありません。setupをasyncにしてawaitでファイルを取得しましょう。gltfは内容的にはjsonなのでloadJSONでOKです。いずれ、来年の夏ごろには2系がデフォルトになるそうなので慣れておくといいでしょう。
カメラ
カメラについてですが、gltfの解析で得られるモデルをBlenderの見た目のまま素直に閲覧したいんで、p5特有の(特有の)上下逆転仕様は邪魔なんですね。それで去年の記事でやったようにfrustumを使ってそこら辺をいじってます。
// 去年も使ったやつ
// blenderと同じ見た目にするため、まずy軸正方向を上に取る
camera(320,320,320, 0,160,0, 0,1,0);
const eyeDist = dist(320,320,320, 0,160,0);
// そのうえでfrustumモードを使い、上下を逆転させ、射影行列の-1を殺す。
const nearPlaneHeightHalf = eyeDist*tan(PI/6)*0.1;
const nearPlaneWidthHalf = nearPlaneHeightHalf*width/height;
// ここの第3,4引数。
frustum(-nearPlaneWidthHalf, nearPlaneWidthHalf, nearPlaneHeightHalf, -nearPlaneHeightHalf, 0.1*eyeDist, 10*eyeDist);
あとはorbitControlの第二引数に-1を設定して終わりです。
補足は以上です。
おわりに
ここまでお読みいただいてありがとうございました。





