はじめに
p5.Geometryをモチーフに、セッターとゲッターの理解を深める企画。
セッターとはオブジェクトのプロパティを代入式の形で書き換えて関数化するもの、ゲッターとは値の取得を関数化するもの。ざっくり。上のレファレンスを見ると分かるがオブジェクト一般の話であり、自分にはわかりにくいので、クラス表記の話にします。時代はclassなので、使わない理由が無いです。使おう。
p5.Geometry
ジオメトリを独自に構築するp5の補助機能です。
頂点をverticesで作って法線をvertexNormalsで作って面をfacesで作って...とかして最後にmodelをかますと描画される便利機能です。これを題材にして色々遊びます。
コード
function setup() {
createCanvas(400,400,WEBGL);
background(100);
//const vError = new VertexError(); // だめ!!!!
//vError.p = [1,2,3];
//console.log(vError.p);
const v = new Vertex();
v.p = [1,2,3];
console.log(v.p.array());
const geom = new p5.Geometry();
const vs = [];
for(let i=0;i<4;i++){vs.push(new Vertex())}
for(let i=0; i<4; i++){
vs[i].p = [200*(i%2)-100, 200*(floor(i/2))-100];
vs[i].n = [0,0,1];
}
geom.vertices.push(...vs.map(v=>v.p));
geom.vertexNormals.push(...vs.map(v=>v.n));
geom.faces = [[0,1,2],[2,1,3]];
fill('red');
pointLight(255,255,255,0,0,40);
specularMaterial(30);
//model(geom);
const geom2 = new p5.Geometry();
const vs2 = [];
for(let i=0;i<4;i++){vs2.push(new ColoredVertex())}
const palette = [[0,0,0.5], [0,0,1], [0,0.5,1], [0,1,1]];
for(let i=0; i<4; i++){
vs2[i].p = [200*(i%2)-100, 200*(floor(i/2))-100];
vs2[i].n = [0,0,1];
vs2[i].vc = palette[i];
}
geom2.vertices.push(...vs2.map(v=>v.p));
geom2.vertexNormals.push(...vs2.map(v=>v.n));
geom2.faces = [[0,1,2],[2,1,3]];
geom2.vertexColors.push(...vs2.map(v=>v.vc).flat());
fill(0);
pointLight(255,255,255,0,0,40);
specularMaterial(30);
model(geom2);
}
class VertexError{
constructor(){
// この時点で既にエラーを食らう。やるなよ...絶対やるなよ...
this.p = createVector(0,0,0);
this.n = createVector(0,0,0);
}
set p(value){
if(Array.isArray(value)){
const a = value.slice();
if(a.length < 3){ a.push(...Array(3-a.length).fill(0)); }
this.p.set(a);
}else{
this.p.set(value);
}
}
set n(value){
if(Array.isArray(value)){
const a = value.slice();
if(a.length < 3){ a.push(...Array(3-a.length).fill(0)); }
this.n.set(a);
}else{
this.n.set(value);
}
}
get p(){
return this.p;
}
get n(){
return this.n;
}
}
class Vertex{
constructor(){
// これならよい
this.attrs = {
p:createVector(0,0,0),
n:createVector(0,0,0)
}
}
set p(value){
if(Array.isArray(value)){
const a = value.slice();
if(a.length < 3){ a.push(...Array(3-a.length).fill(0)); }
this.attrs.p.set(a);
}else{
this.attrs.p.set(value);
}
}
set n(value){
if(Array.isArray(value)){
const a = value.slice();
if(a.length < 3){ a.push(...Array(3-a.length).fill(0)); }
this.attrs.n.set(a);
}else{
this.attrs.n.set(value);
}
}
get p(){
return this.attrs.p;
}
get n(){
return this.attrs.n;
}
}
class ColoredVertex extends Vertex{
constructor(){
super();
this.attrs.vc = [1,1,1,1];
}
set vc(value){
const a = value.slice();
// 色なのでfillingは1にしましょう
if(a.length < 4){ a.push(...Array(4-a.length).fill(1)); }
for(let k=0;k<4;k++){
this.vc[k] = a[k];
}
}
get vc(){
return this.attrs.vc;
}
}
p5.Geometryの構造
頂点はverticesで決めます。p5ベクトルの配列です。法線はvertexNormalsで決めます。p5ベクトルの配列です。これらは配列の形になっており、indexを元に紐付けられていて、頂点には確かに法線が対応していますが、いささか扱いづらい場合があるでしょう。そこで、一つのクラスに頂点と法線をまとめることを考えました。名付けて「Vertexクラス」です。Vertexクラスには2つのプロパティを想定しています。pとnです。pはpositionのpでnはnormalのnです。
// こんなの
constructor(){
// この時点で既にエラーを食らう。やるなよ...絶対やるなよ...
this.p = createVector(0,0,0);
this.n = createVector(0,0,0);
}
とりあえず文字列は無視してください。
こういう風に用意するのが自然ですね。pで頂点が出てnで法線が出ます。ところでゲッターとセッターですが、これは次のように用意したいと考えています。
set p(value){
if(Array.isArray(value)){
const a = value.slice();
if(a.length < 3){ a.push(...Array(3-a.length).fill(0)); }
this.p.set(a);
}else{
this.p.set(value);
}
}
set n(value){
if(Array.isArray(value)){
const a = value.slice();
if(a.length < 3){ a.push(...Array(3-a.length).fill(0)); }
this.n.set(a);
}else{
this.n.set(value);
}
}
get p(){
return this.p;
}
get n(){
return this.n;
}
ベクトルのセット関数はベクトルや配列や列挙が入ります。ところでセッターですが、セッターの制約上取れる引数は1つだけです。つまり通常もっとも使うケースである「列挙」による指定はできません。しかしそれだけのことであり、配列は列挙扱いですから、さしたる問題はありません。
なお内容的には、長さが3に満たない場合は0埋めをしています。自然だと思います。x,y,0を想定してxとyだけ指定するのは自然でしょう?そこでこのような仕様にしました。こうすると、
v.p = [1,2];
と書いた場合に1,2,0がセットされるわけです。
ゲッターについては単純にpとnを返します。要するにセットする過程やゲットする過程を代入方式で書けるわけですから、とても便利ですね。そしてクラスができました。
Vertexクラス(?)
class VertexError{
constructor(){
// この時点で既にエラーを食らう。やるなよ...絶対やるなよ...
this.p = createVector(0,0,0);
this.n = createVector(0,0,0);
}
set p(value){
if(Array.isArray(value)){
const a = value.slice();
if(a.length < 3){ a.push(...Array(3-a.length).fill(0)); }
this.p.set(a);
}else{
this.p.set(value);
}
}
set n(value){
if(Array.isArray(value)){
const a = value.slice();
if(a.length < 3){ a.push(...Array(3-a.length).fill(0)); }
this.n.set(a);
}else{
this.n.set(value);
}
}
get p(){
return this.p;
}
get n(){
return this.n;
}
}
実行してはいけない
このクラスを作って使おうとしないでください。エラーを食らいます。
const vError = new VertexError(); // だめ!!!!
mySketch, line 71:too much recursion
まずコンストラクタにthis.p=と書いた時点でこの処理を実行するにはセッターが必要です。それでセッターを見ると、セッター内部でthis.pがセット関数を実行していますね。それにはthis.pが必要ですから、ここでゲッターが呼ばれます。ここが引き金になります。
ゲッターで取得しようとするんですが、ゲットしようにもゲッターにはthis.pと書いてありますから、これをゲットしないといけないので、そのためにまたゲッターが呼ばれるんですが、ゲッターでそれを取得するためにまたゲッターが必要ですから、キリがないわけで、パンクするんですね。too much recursionとはそんなような意味です。
回避策
ではどうすればいいかというと、こうします:
class Vertex{
constructor(){
// これならよい
this.attrs = {
p:createVector(0,0,0),
n:createVector(0,0,0)
}
}
set p(value){
if(Array.isArray(value)){
const a = value.slice();
if(a.length < 3){ a.push(...Array(3-a.length).fill(0)); }
this.attrs.p.set(a);
}else{
this.attrs.p.set(value);
}
}
set n(value){
if(Array.isArray(value)){
const a = value.slice();
if(a.length < 3){ a.push(...Array(3-a.length).fill(0)); }
this.attrs.n.set(a);
}else{
this.attrs.n.set(value);
}
}
get p(){
return this.attrs.p;
}
get n(){
return this.attrs.n;
}
}
まずpとnをattrsという形でプロパティとして用意します。セッターは、その中のpやnにセットする関数として実装します。つまり、
this.p = [1,2];
と書いた場合、1,2,0はthis.pではなく、これはセッター兼ゲッターですから、まあプロパティではなくて、this.attrs.pにセットされるわけですね。ゲッターもattrsのpが取り出されます。nも同様です。こういう風にすることで、参照のループを防げますし、またpを出すのにthis.attrs.pなどと長ったらしく書く必要は無いです。もちろんそう書いても取得できるんですが、ゲッターを使えばthis.pでポンと出せます。
また、ゲッターは関数ですから、加工もできます。但し引数は取れないので、加工の仕方は1つだけです。
p5.Geometryを作ってみよう
このクラスでp5.Geometryを作ってみましょうか。
const geom = new p5.Geometry();
const vs = [];
for(let i=0;i<4;i++){vs.push(new Vertex())}
for(let i=0; i<4; i++){
vs[i].p = [200*(i%2)-100, 200*(floor(i/2))-100];
vs[i].n = [0,0,1];
}
geom.vertices.push(...vs.map(v=>v.p));
geom.vertexNormals.push(...vs.map(v=>v.n));
geom.faces = [[0,1,2],[2,1,3]];
fill('red');
pointLight(255,255,255,0,0,40);
specularMaterial(30);
model(geom);
pとnにセッターで値をはめています。こうして道具が揃うので、あとはmap関数とゲッターでちょちょいっとベクトルを出します。これで完成です。あとはmodelで描画するだけ。
色を付けてみよう
Vertexクラスを拡張して色が付くようにしました。頂点ごとに決められるので、頂点データや法線データで色を決めてもいいですね。今回は実験なので、雑にパレットを作ってそこから取りました。
class ColoredVertex extends Vertex{
constructor(){
super();
this.attrs.vc = [1,1,1,1];
}
set vc(value){
const a = value.slice();
// 色なのでfillingは1にしましょう
if(a.length < 4){ a.push(...Array(4-a.length).fill(1)); }
for(let k=0;k<4;k++){
this.vc[k] = a[k];
}
}
get vc(){
return this.attrs.vc;
}
}
vcを追加し、さらにセッターとゲッターも合わせて追加しました。
const geom2 = new p5.Geometry();
const vs2 = [];
for(let i=0;i<4;i++){vs2.push(new ColoredVertex())}
const palette = [[0,0,0.5], [0,0,1], [0,0.5,1], [0,1,1]];
for(let i=0; i<4; i++){
vs2[i].p = [200*(i%2)-100, 200*(floor(i/2))-100];
vs2[i].n = [0,0,1];
vs2[i].vc = palette[i];
}
geom2.vertices.push(...vs2.map(v=>v.p));
geom2.vertexNormals.push(...vs2.map(v=>v.n));
geom2.faces = [[0,1,2],[2,1,3]];
geom2.vertexColors.push(...vs2.map(v=>v.vc).flat());
fill(0);
pointLight(255,255,255,0,0,40);
specularMaterial(30);
model(geom2);
よく読むと分かりますがセッターで長さ3の場合に1で埋めています。そしてp5.Geometry構成時にもパレットには長さ3の配列しか入っていません。長さ3の配列をセットしたにも関わらず末尾が1の長さ4の配列が結果的にセットされています。まあvertexColorsで透明度をいじる機会はほぼ無いでしょうから、この方が指定しやすいでしょう。工夫すれば文字列から色が決まるようにしたりできます。セッターの可能性は無限大です。
参考になれば幸いです。
おわりに
ここまでお読みいただいてありがとうございました。
補足
現行の仕様で、0,0,0でベクトルを作った後、座標を(1,2,0)にしようと思って[1,2]で指定し、そのあとでz軸正方向に動かそうと思って0,0,4でaddした場合にフレンドリーエラーが出ることについて触れておきます。
補足2
p5ベクトルの初期化でわざわざ0,0,0と0を3個も書いた理由はこれです。
重箱の隅をつつく趣味は無いんですが、わざわざFriendlyErrorを無効化するのは手間だと思います...



