前回、webkitCompassHeading
から北を求める方法を明らかにしたが、実はまだ足りない。
またまたAndroidは実機を持ってないので、どうなってるか分からない。
absolute
がtrue
になってれば問題ない気がするけど。
webkitCompassHeading
は真北を返してくれない
@@ -9,89 +9,11 @@
<script src="thick-line.js"></script>
<script src="spherical-grid.js"></script>
-<script id="biface.fs" type="text/plain">
-uniform lowp vec4 uFront, uBack;
-void main(void) {
- gl_FragColor = mix(uBack, uFront, float(gl_FrontFacing));
-}
-</script>
<script>
"use strict";
-function $QSA(...args) { return $D.querySelectorAll(...args); }
-const Vec4 = buildClass(Array, {
-factory: {
-//----- static members -----
-...VectorFromArray,
-XYZ: [ 0, 1, 2 ],
-create(...args) {
- const me = this.fromArray(...args) ?? [ 0, 0, 0, 1 ];
- this.takeover(me, ...args);
- return me;
-}
-},
-//===== instance members =====
-init() {
- this.length = 4;
- for (const k of this.keys())
- this[k] ??= 0;
-},
-set x(val) { this[0] = val; },
-get x() { return this[0]; },
-set y(val) { this[1] = val; },
-get y() { return this[1]; },
-set z(val) { this[2] = val; },
-get z() { return this[2]; },
-set w(val) { this[3] = val; },
-get w() { return this[3]; },
-norm(swiz = Vec4.XYZ) {
- return Math.hypot(...swiz.map(i => this[i]));
-},
-normalize(swiz = Vec4.XYZ) {
- const r = this.norm();
- swiz.forEach(i => this[i] /= r);
- return this;
-}
-});
-const Mat4 = buildClass(VanillaMat4, {
-factory: {
-create(...args) {
- return this.base.create.call(this, ...args);
-},
-rotZuvecXto(x, y) {
- return this.getFactory().create([
- x, y, 0, 0,
- -y, x, 0, 0,
- 0, 0, 1, 0,
- 0, 0, 0, 1
- ]);
-},
-rotXuvecZto(y, z) {
- return this.getFactory().create([
- 1, 0, 0, 0,
- 0, z, -y, 0,
- 0, y, z, 0,
- 0, 0, 0, 1
- ]);
-}
-},
-affect(mat, ...args) {
- super.affect(mat);
- if (args.length < 1)
- return this;
- return this.affect(...args);
-},
-affected(...args) {
- return this.clone().affect(...args);
-},
-scaled(...args) {
- return this.clone().scale(...args);
-}
-});
-[ "rotZuvecXto", "rotXuvecZto" ]
-.forEach(v => Mat4.prototype[v] = function (...args) {
- return this.affect(this.factory[v].call(this, ...args));
-});
+const Vec4 = VanillaVec4;
+const Mat4 = VanillaMat4;
-VanillaMat4.setToRadian(deg2rad);
-VanillaMat4.aspectMode = "min";
+Mat4.setToRadian(deg2rad);
+Mat4.aspectMode = "min";
function prepareRing(gl, w, m = 360) {
function pos(r, i) {
@@ -127,8 +49,8 @@ $DCL(_ => {
gl.fetchShader("fs", "roundtex.fs"),
gl.fetchShader("fs", "roundborder.fs"),
+ gl.fetchShader("fs", "biface.fs"),
gl.loadTexture("iphone13-front.jpg"),
gl.loadTexture("iphone13-back.jpg")
);
- const fsPlate = gl.loadShader("fs", $ID("biface.fs"));
const devWidth = 0.35, devAspect = 2, devRadiusX = 0.15, devBorderX = 0.05;
const devRadius = [ devRadiusX, devRadiusX / devAspect ];
@@ -153,5 +75,5 @@ $DCL(_ => {
let render;
ready.then((
- vs, vsTex, vsLine, fs, fsDisk, fsTex, fsBorder, front, back
+ vs, vsTex, vsLine, fs, fsDisk, fsTex, fsBorder, fsPlate, front, back
) => {
const prog = gl.linkProgram(vs, fs);
Mat4
がVanillaMat4
そのものになったので、setToRadian
・aspectMode
もMat4
に対して設定する
biface.fs
もファイルに追い出す
@@ -192,4 +192,25 @@ $DCL(_ => {
render?.request();
});
+ navigator.geolocation.watchPosition(loc => {
+ const pos = loc.coords;
+ $QSA("[data-loc-id]").forEach(e => {
+ e.innerText = numfmt3(pos[e.dataset.locId]);
+ });
+ }, err => {
+ console.log(err.message);
+ let s = "UNKNOWN";
+ switch(err.code) {
+ case 1:
+ s = "NOPERM";
+ break;
+ case 2:
+ s = "NOTAVAIL";
+ break;
+ case 3:
+ s = "TIMEOUT";
+ break;
+ }
+ $QS("[data-loc-id=latitude]").innerText = s;
+ }, { enableHighAccurasy: true });
});
</script>
@@ -202,8 +223,8 @@ p:has(.device-orientation-enabled) {
display: none;
}
-[data-num-id], #z_z {
+[data-num-id], [data-loc-id], #z_z {
font-family: "azeret mono", monospace;
}
-[data-num-id], #z_z {
+[data-num-id], [data-loc-id], #z_z {
white-space: pre;
}
@@ -231,4 +252,7 @@ p:has(.device-orientation-enabled) {
<tr><td>heading<td data-num-id="webkitCompassHeading">
<tr><td>Z.z<td id="z_z">
+<tr><td>lat<td data-loc-id="latitude">
+<tr><td>lng<td data-loc-id="longitude">
+<tr><td>alt<td data-loc-id="altitude">
</table>
</div>
navigator.geolocation
にアクセスすることでユーザーに許可を求める表示が出る
calcDeclination
がほぼ使い方そのもの
new Geomag(COF);
はNOAAのモデルが変わらない限り、起動時に1度だけやればよい
calculate
を呼び出すと地磁気に関するデータを計算してくれる
calc
とmag
がcalculate
のエイリアスとして定義されている
new Date()
が返すのと同じ仕様)
dec
が偏角(度)で、東偏がプラス
rotZ
の角度で負の方向に回す必要がある
webkitCompassHeading
にdec
を足せばよい
@@ -9,4 +9,5 @@
<script src="thick-line.js"></script>
<script src="spherical-grid.js"></script>
+<script src="wmmacc2.js"></script>
<script>
"use strict";
@@ -74,4 +75,5 @@ $DCL(_ => {
let matHeading = Mat4.create();
let render;
+ let magdec = 0;
ready.then((
vs, vsTex, vsLine, fs, fsDisk, fsTex, fsBorder, fsPlate, front, back
@@ -164,5 +166,5 @@ $DCL(_ => {
if (evDev != null) {
matAtt = Mat4.rotY(evDev.gamma).rotX(evDev.beta).rotZ(evDev.alpha);
- matHeading = Mat4.rotZ(evDev?.webkitCompassHeading ?? 10);
+ matHeading = Mat4.rotZ((evDev?.webkitCompassHeading ?? 10) + magdec);
}
gl.initCanvas();
@@ -192,9 +194,13 @@ $DCL(_ => {
render?.request();
});
+ const geomag = new Geomag(COF);
navigator.geolocation.watchPosition(loc => {
const pos = loc.coords;
+ const mag = geomag.calc(pos.latitude, pos.longitude, pos.altitude / 1000);
+ magdec = mag.dec;
$QSA("[data-loc-id]").forEach(e => {
e.innerText = numfmt3(pos[e.dataset.locId]);
});
+ $ID("magdec").innerText = numfmt3(magdec);
}, err => {
console.log(err.message);
@@ -223,8 +229,8 @@ p:has(.device-orientation-enabled) {
display: none;
}
-[data-num-id], [data-loc-id], #z_z {
+[data-num-id], [data-loc-id], #z_z, #magdec {
font-family: "azeret mono", monospace;
}
-[data-num-id], [data-loc-id], #z_z {
+[data-num-id], [data-loc-id], #z_z, #magdec {
white-space: pre;
}
@@ -255,4 +261,5 @@ p:has(.device-orientation-enabled) {
<tr><td>lng<td data-loc-id="longitude">
<tr><td>alt<td data-loc-id="altitude">
+<tr><td>magdec<td id="magdec">
</table>
</div>
これまたiOSの話。
@@ -70,8 +70,10 @@ $DCL(_ => {
const matNorth = Mat4.scale(0.1, 0.5).trans(0, 0.5);
let evDev = null;
+ let evHeading = null;
let matAtt = Mat4.create();
let matGrid = Mat4.create();
let matVP = Mat4.create();
- let matHeading = Mat4.create();
+ let matHeadingUp = Mat4.create();
+ let matHeadingDn = Mat4.create();
let render;
let magdec = 0;
@@ -141,7 +143,15 @@ $DCL(_ => {
matDirUp.rotXuvecZto(-Z.y, Z.z).rotZuvecXto(A.x, A.y);
matDirDn.rotXuvecZto(Z.y, -Z.z).rotZuvecXto(A.x, A.y);
- for (const v of [
- { mat: matDirUp, color: [ 0, 0.8, 0.8 ], alpha: [ 1, 0.4 ] },
- { mat: matDirDn, color: [ 1, 0.5, 0.5 ], alpha: [ 0.5, 1 ] }
+ let newHeading = evDev?.webkitCompassHeading ?? 10;
+ if (newHeading == evHeading)
+ newHeading = null;
+ else
+ evHeading = newHeading;
+ for (const v of [ {
+ mat: matDirUp, matHeading: matHeadingUp,
+ color: [ 0, 0.8, 0.8 ], alpha: [ 1, 0.4 ]
+ }, {
+ mat: matDirDn, matHeading: matHeadingDn,
+ color: [ 1, 0.5, 0.5 ], alpha: [ 0.5, 1 ] }
]) {
const a = v.alpha[ upsideDown ? 1 : 0 ];
@@ -156,5 +166,7 @@ $DCL(_ => {
prog.drawArrays("aPos:f2", trianglePlate, gl.TRIANGLE_STRIP);
progLine.use();
- progLine.setUniform("uMat:m4", matNorth.affected(v.mat, matHeading, matVP));
+ if (newHeading)
+ v.matHeading.splice(0, Infinity, ...v.mat.rotatedZ(newHeading));
+ progLine.setUniform("uMat:m4", matNorth.affected(v.matHeading, matVP));
progLine.setUniform("uColor", ...v.color, a);
drawRoundCapLine(progLine, triangleFrame, [ "min", 10 ], "f2");
@@ -164,8 +176,6 @@ $DCL(_ => {
}
render = VanillaFrameRenderer.create(_ => {
- if (evDev != null) {
+ if (evDev != null)
matAtt = Mat4.rotY(evDev.gamma).rotX(evDev.beta).rotZ(evDev.alpha);
- matHeading = Mat4.rotZ((evDev?.webkitCompassHeading ?? 10) + magdec);
- }
gl.initCanvas();
matVP = Mat4.rotX(-75).affect(matGrid)
webkitCompassHeading
+磁気偏差を適用したものが北標示のモデル行列
webkitCompassHeading
が更新された時だけ更新する
v.matHeading
を設定しているところ
v.matHeading
はZ方向上向き(matHeadingUp
)・下向き(matHeadingDn
)どちらかの行列に設定される
=
で書き換えるとv.matHeading
が新しい行列インスタンスに置き換わり、matHeadingUp
などは置いてけぼりになる(更新されない)
v.matHeading
が指しているインスタンスを直接書き換える必要がある
Array.prototype.splice
が使える
@@ -15,4 +15,23 @@ const Vec4 = VanillaVec4;
const Mat4 = VanillaMat4;
+const GL = buildClass(VanillaGL, {
+factory: {
+create(canvas, opt, ...args) {
+ const glopt = {};
+ if (opt.useStencil)
+ glopt.stencil = true;
+ return this.takeover($ID(canvas).getContext("webgl", glopt), opt, ...args);
+}
+},
+init(opt) {
+ super.init(opt);
+ if (opt.useStencil)
+ this.clearMask |= this.STENCIL_BUFFER_BIT;
+},
+colorMaskAll(b) {
+ this.colorMask(b, b, b, b);
+}
+});
+
Mat4.setToRadian(deg2rad);
Mat4.aspectMode = "min";
@@ -41,5 +60,7 @@ function drawVector(prog, line, vec, mat, opt) {
}
$DCL(_ => {
- const gl = VanillaGL.create($QS("canvas"), { useDepth: true });
+ const gl = GL.create($QS("canvas"), {
+ useDepth: true, useStencil: true
+ });
const ready = prepare(
gl.fetchShader("vs", "thru.vs"),
@@ -59,5 +80,8 @@ $DCL(_ => {
const matDev = Mat4.scale(devWidth, devWidth * devAspect);
const matHead = Mat4.trans(0, -1.2).scale(0.07).trans(0, matDev.get(1, 1) - devBorder[1]);
+ const stencilBorder = 0.2;
+ const stencilSize = [ 1 + stencilBorder, 1 + stencilBorder / devAspect];
const line = prepareRoundCapLine(gl);
+ const line2 = prepareRoundCapLine(gl, [ [ -1 ], [ 1 ] ]);
const plate = gl.createFloatBuffer([
[ -1, -1, 0, 1 ], [ 1, -1, 1, 1 ], [ -1, 1, 0, 0 ], [ 1, 1, 1, 0 ]
@@ -173,5 +197,18 @@ $DCL(_ => {
}
- drawVector(progLine, line, A, matVP, { color: [ 1, 1, 0, 1 ] });
+ const mat = upsideDown ? matDirDn : matDirUp;
+ gl.enable(gl.STENCIL_TEST);
+ gl.colorMaskAll(false);
+ gl.stencilFunc(gl.ALWAYS, 1, ~0);
+ gl.stencilOp(gl.KEEP, gl.KEEP, gl.REPLACE);
+ progBorder.use();
+ progBorder.setUniform("uMat:m4", matDev.scaled(...stencilSize).affect(mat, matVP));
+ progBorder.setUniform("uBorder", 0.5, 0.5, ...devRadius);
+ drawPlate(progBorder);
+ gl.colorMaskAll(true);
+ gl.stencilFunc(gl.LESS, 0, 1);
+ gl.stencilOp(gl.KEEP, gl.KEEP, gl.KEEP);
+ drawVector(progLine, line2, A, matVP, { color: [ 1, 1, 0, 1 ] });
+ gl.disable(gl.STENCIL_TEST);
}
render = VanillaFrameRenderer.create(_ => {
getContext
するときにオプションを設定しないといけないため、VanillaGL.create
を置き換える
init
ではステンシルバッファのクリアビットを立てておくが、有効にはしない(深度テストの場合はinit
で有効にしている)
colorMaskAll
という関数も作っておく
colorMask(false, false, false, false)
と書くのが面倒なので
devAspect
で調整する
line2
を定義
drawPlate
)→描画有効、ステンシル適用→線を描く(drawVector
)→ステンシル無効
半透明オブジェクトが重なっている場合、深度テストを有効にすると重なったオブジェクトのうちどちらかが描画されない。 実際、サンプルプログラムでも地面の向こう側に隠れた端末が見えていない。 では深度テストを無効にすればよいかというとそんなに簡単ではない。
アルファによる色の計算は、元々その場にあった色を \(d\) 、これから描く色を \(s\) とすると、実際に描かれる色は \((1-a)d + as\) と表現できる(色ごとに計算する)。
物体Aがアルファ0.7の赤、物体Bがアルファ0.7の緑だったとする。 背景が白で、その上に物体Aが重なれば、 \(d\) はRGB=(1, 1, 1)、 \(s\) はRGB=(1, 0, 0)だから、描かれる色はRGB=(1, 0.3, 0.3)になる。 その上に物体Bが重なると、 \(s\) はRGB=(0, 1, 0)だから、最終的にはRGB=(0.3, 0.79, 0.09)になる。
これが逆に重なると、ご想像のとおりRGB=(0.79, 0.3, 0.09)になり、赤と緑の濃さが逆になる。 日常でも色合いの異なる半透明のものが重なっている場合、どちらが手前にあるかは実際に見えている色合いで判断できるが、これを計算で示したことになる。
これだけなら順序正しく奥から描画すればいいだけだが、今回の場合は地面と端末が軸を境にして前後が入れ替わるので、単純にどちらかを奥、と決められない。 理屈の上では端末手前側、地面、端末奥側、と描画すればよいが、交差位置が端末の姿勢によって変わるため、どこまでが手前でどこから奥かを計算するのが非常に面倒である。
半透明の面が重なっているとこの問題に突き当たる。 条件によってはデザイナーのための半透明の描画順(KLab)のような対応もできる。 満たしていなければいけない条件は 「ポリゴンの各面から任意のふたつを選び出し、その面を延長して交わった線の両側にまたがってポリゴンが描画されていないこと」 である。 竜巻の例の場合、ポリゴンの各面はざっくり円筒の接平面になる。 接平面が交わってできる直線は必ず円筒の外にあり、すべてのポリゴンはこの直線より手前で終わると保証できる。 この条件を満たさない場合は視点の位置(見る方向)によって描画順を変更しなければならない。
この図で斜めの線2本はポリゴンを真横から見た状態、丸数字は描画順である。 ひとつのポリゴンに対して丸数字がふたつ描かれているが、両面を描画する、という意味である。 矢印は視線方向を表している。 例えば右向き矢印ならばまず1の面が描画され、続いて2の面が描画される。 3と4はカリングされて描画されず、これで所望の結果になる。 左向きの場合は1・2はカリングされて描画されず、3・4がこの順で描画されるのでやはり所望の結果になる。
もし、両方の平面が交点Pを超えていなければこれで問題ない。 しかし、いずれか一方でも交点Pを超えてしまうと、垂直方向の矢印のような重なりが発生する。 この場合は固定の描画順では対応できない。 たとえば、上から見た場合(下向き矢印)では1・3がこの順で描かれるため、手前にある1が奥にあるような描画結果になってしまう。
一般的にはデプスピーリング(床井研究室)のような手法が使える。 デプスピーリングで深度値を奥に向かって求めていき、逆に奥側から深度テスト有効でポリゴンを描画していけばよく、機械的に描画が可能である。 これならばどれだけ面が重なっていても平気だが、重なりが多いほど描画回数が増えるため、消費メモリ量・負荷とも増大する。 求める深度値は半透明オブジェクトの数とは限らない。 どこから見ても絶対に重ならないのならデプスピーリングをする必要はないし、ふたつのオブジェクトが2回以上重なって見える(先ほどの竜巻の例は二重円筒なので最大4つの面が重なっている)と、オブジェクトの数よりも求める深度値の方が多くなる。 逆に、不透明な物体に関しては普通に深度テストが効くため、デプスピーリングの対象に含めることもないが、透けて見えている部分はそれより手前にある半透明オブジェクトの重なりの順序・回数の影響を受けるため、後半戦で半透明オブジェクトを描くごとに不透明なオブジェクトを描き直す必要がある。
要するに半透明の面が重なるとチョー面倒くさいのだが、重なっている面が最大2面の場合は手抜きができる。 今回の場合は交差するオブジェクトが端末と地面だけで、どちらも平面なので視線方向で2回以上重なることもなく、この条件を満たしている。 指標類を半透明オブジェクトの仲間に含めなかったのはこの条件を満たすためである。
やり方は深度値がひとつしかないデプスピーリングと同じだが、深度値の保持に深度バッファをそのまま使う。 以下のような手順になる。
colorMask
関数で描画結果が描かれないようにし、深度バッファをクリアし、少し奥の位置に半透明部分を描く
depthFunc
で深度テストを「深度値と同じか、奥にある物体」(GEQUAL
)に設定して半透明オブジェクトを描く
depthFunc
で深度テストを「深度値より手前」(LESS
、つまり普通の状態)にして不透明な部分を描く
少し奥の深度値を使うのがポイントである。 奥の面を描いたときに、描画された部分の深度値は奥の面(右側太線、黒)、描画されていない部分は手前の面(左側太線、赤)の値になっている。 このため、そのままの位置に深度値を残してしまうと、手前の面を描画するときに、
LESS
)にすると、重なりのない手前の面が描画されない
LEQUAL
)にすると、奥にある物体がもう一度描かれてしまう
という結果になってしまう。 深度値を奥にオフセットしておけば、重なりのない手前の面は自分自身の深度値の「少し奥」と比較することになり、きちんと描画される。
この方法では不透明・半透明なオブジェクトはそれぞれを分けて複数回描画する必要がある。
北はこっち!で不透明なオブジェクトと半透明なオブジェクトの描画をdrawOpaque
とdrawTranslucent
という関数に分けたのはそういう事情である。
コード上ではこのうちdrawTranslucent
にdepth
という引数を追加して、これがtrue
ならばpolygonOffset
でポリゴンを奥へオフセットしている。
drawTranslucent
ではZファイティングを防ぐために赤道と端末を少し手前にオフセットしている。
深度値の計算時は普通に平面として計算すればよいので、オフセットの合計を求めるのではなく、depth
がtrue
の時は単純に手前へのオフセットをしないようにしている。
以下にここまでの結果を示す。
09 Sep 2024: 本編
31 Aug 2024: 予告編
Copyright (C) 2024 akamoz.jp