p5.jsのwebglでスクリーン座標と深度値を取得する

# はじめに
　p5.jsのwebglで深度値を計算する。深度値というのはwebGLにおいて、その点の深さのこと。正規化デバイス座標に落とす際、遠近感を表現する値が計算される。これの$z$座標が深度値で、最も近くには-1が、最も遠くには1が割り当てられる（webGLの場合）。それを外部で特定の3次元座標に対して計算する。これにより、スクリーン上での2次元の位置と、深さが取得できる。

## コード全文
※p5.js/1.11.2
[playWithNDC2](https://openprocessing.org/sketch/2220025)
```js
// NDC. 簡単。
/*
1.11.2
*/
let info;
let eye;

let vectors = [];

function setup() {
  createCanvas(800, 600, WEBGL);
  pixelDensity(1);
  info = createGraphics(800, 600);
  info.noStroke();
  noStroke();
  camera(0,0,height*0.5*sqrt(3),0,0,0,0,1,0);
  perspective(PI/3,width/height,height*0.05*sqrt(3),height*5*sqrt(3));

  for(let i=0; i<10; i++){
    vectors.push(createVector(random(400)-200, random(400)-200, random(400)-200));
  }
  eye = createVector();
}

function draw() {
  const cam = this._renderer._curCamera;
  
  orbitControl(1,1,1,{freeRotation:true});
  eye.set(cam.eyeX, cam.eyeY, cam.eyeZ);
  background(128);

  ambientLight(128);
  const front = cam.cameraMatrix.createSubMatrix3x3().row(2);
  directionalLight(128, 128, 128, -front.x, -front.y, -front.z);
  specularMaterial(64);

  for(let i = 0; i<10; i++){
    const v = vectors[i];
    push();
    translate(v.x,v.y,v.z);
    rotateX(frameCount*TAU/360 + noise(0,i)*200);
    rotateY(frameCount*TAU/240 + noise(i,0)*200);
    box(20);
    pop();
  }

  info.clear();
  info.textAlign(CENTER, CENTER);
  info.noStroke();
  info.fill(0);
  info.textSize(24);

  for(let i=0; i<10; i++){
    const v = vectors[i];
    const ndc = getNDC(v);
    const distWithBoxAndEye = eye.dist(v);
    const sizeFactor = height*0.6*sqrt(3)/distWithBoxAndEye
    info.textSize(sizeFactor*20);
    const alpha = 255*pow(1.0-ndc.z,0.3);
    info.fill(0, alpha);
    info.text(ndc.z.toFixed(2),ndc.x,ndc.y); // 近くで0, 遠くで1
  }
  
  info.textAlign(LEFT, TOP);
  info.fill(255);
  info.textStyle(NORMAL);
  info.textSize(18);
  info.text(frameRate().toFixed(3), 5, 5);
  // 目の位置
  info.text(eye.x.toFixed(3),5,25);
  info.text(eye.y.toFixed(3),5,45);
  info.text(eye.z.toFixed(3),5,65);
  //drawImage(info);

  // 最近はこうしてる
  push();
  noLights();
  camera(0,0,1,0,0,0,0,1,0);
  ortho(-1,1,-1,1,0,1);
  translate(0,0,1);
  texture(info);
  plane(2)
  pop();
}


function getNDC(v){
  // vとカメラのビューからビューを出す
  // それと射影からあれを出す
  const _gl = this._renderer;
  // この関数は与えられたベクトルvにuMVを適用してカメラ座標系に落とす処理
  // 名称はpagurekが雑に名付けたものです
  v = _gl._curCamera.cameraMatrix.multiplyPoint(v);
  // これにより正規化デバイス座標系に落ちるんだよね
  v = _gl._curCamera.projMatrix.multiplyAndNormalizePoint(v);
  // それをキャンバスに読み替えるんだよね
  const _x = (0.5 + 0.5 * v.x) * width;
  const _y = (0.5 - 0.5 * v.y) * height;
  const _z = 0.5 + 0.5 * v.z;
  return {x:_x, y:_y, z:_z}
}
```
実行結果。このデモでは遠くに行くほど透明度を落としている。

![efefvegr.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/437349/7a5b39f0-1f26-226b-6883-6c79e6d3d475.png)

この手のデモではカメラをぐりぐり動かした方が動作確認しやすいので、orbitControlが便利である。

## 深度値の計算パート

```js
function getNDC(v){
  // vとカメラのビューからビューを出す
  // それと射影からあれを出す
  const _gl = this._renderer;
  // この関数は与えられたベクトルvにuMVを適用してカメラ座標系に落とす処理
  // 名称はpagurekが雑に名付けたものです
  v = _gl._curCamera.cameraMatrix.multiplyPoint(v);
  // これにより正規化デバイス座標系に落ちるんだよね
  v = _gl._curCamera.projMatrix.multiplyAndNormalizePoint(v);
  // それをキャンバスに読み替えるんだよね
  const _x = (0.5 + 0.5 * v.x) * width;
  const _y = (0.5 - 0.5 * v.y) * height;
  const _z = 0.5 + 0.5 * v.z;
  return {x:_x, y:_y, z:_z}
}
```
　具体的には特定のベクトルの座標をビュー変換して、さらにプロジェクション変換すればいい。それに使う関数が存在する。この関数はorbitControlを実装する際に、平行移動を実装するために用いられたものである。真ん中で実行している計算がそれ。その結果得られる_x,_y,_zは何を意味しているかというと、まず_xが左で-1、右で1になる。次に_yが-1で下、1で上になる。最後に_zは近くで-1、遠くで1になる。そこでそれらすべてを0から1の範囲に落として分かりやすくしている。このデータを使えばスクリーン上の位置を特定できる。

　書いていて気が付いた。これスクリーン座標取得する方が本筋じゃないの...まあいいか。

　正規化デバイス座標について：wgldのこの記事が参考になる。[深度値と座標系について理解する](https://wgld.org/d/webgl/w069.html)

# おわりに
　p5にも近々このような関数が実装される予定、だったが、モデル変換にも対応させたいとか、逆変換も用意したいとか、2Dでも使えるようにしたいとかで難航しているらしい。
　[Suggestion of a function to get screen coordinates](https://github.com/processing/p5.js/issues/7059)
　[Solves issue #7059](https://github.com/processing/p5.js/pull/7113)
議論が去年の11月で途切れているので望み薄ですね。

　ここまでお読みいただいてありがとうございました。