Skip to content

Instantly share code, notes, and snippets.

@akirayou
Last active October 17, 2024 12:24
Show Gist options
  • Save akirayou/e84398f74673e28a28949f23772d8efa to your computer and use it in GitHub Desktop.
Save akirayou/e84398f74673e28a28949f23772d8efa to your computer and use it in GitHub Desktop.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<script type="importmap"> { "imports": {
"three": "https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/" } } </script>
<script type="module">
import * as THREE from 'three';
import { VRButton } from 'three/addons/webxr/VRButton.js';
import { PointerLockControls } from 'three/addons/controls/PointerLockControls.js';
const clamp = (n, min, max) => Math.min(Math.max(n, min), max)
//videoタグにUSBカメラ画像を流し込む
function init_video() {
navigator.mediaDevices.enumerateDevices()
.then(function (devices) {
let devs = []
devices.forEach(function (device) {
//console.log(device);
if (device.kind == "video.input") device.deviceId;
devs.push(device.deviceId);
});
const video = document.getElementById("video")
navigator.mediaDevices.getUserMedia({
video: { deviceId: devs[0], width: 1920, height: 1080 },
audio: false,
}).then(stream => {
video.srcObject = stream;
video.play()
}).catch(e => {
console.log(e)
});
})
}
//頂点に対応するテクスチャuv座標を魚眼レンズのパラメータから作成する。
function fishUVbyPos(/** @type{BufferAttribute} */p) {
const CV_H = 1920;
const CV_W = 1080;
//cv2.fisheye.CALIB_FIX_SKEW オプションをつけてαパラメータなしのキャリブレーションがしてあるのが前提
const fx = 642.69595792 / CV_W;//カメラの画角(calib時の画像幅とK行列のfxの比)
const fy = 637.91846877 / CV_H;//カメラの画角から
const D = [0.00647559, -0.01577757, 0.01516903, -0.00629814];//openCV のcv2.fisheye.calibrateのDを与える
const cx = 570.05486897 / CV_W;//カメラの画角(calib時の画像幅とK行列のcxの比)
const cy = 982.26527711 / CV_H;
const N = p.count;
let uv = new Float32Array(N * 2);
for (var n = 0; n < N; n++) {
const x = p.getX(n);
const z = p.getY(n);
const y = p.getZ(n);
const h_xy = (Math.hypot(x, y) + 1e-19);
/* See: https://docs.opencv.org/4.x/db/d58/group__calib3d__fisheye.html*/
var h = Math.atan2(h_xy, z);
h = h * (1 + D[0] * h ** 2 + D[1] * h ** 4 + D[2] * h ** 6 + D[3] * h ** 8);
if (z < 0) h = 5;//OpenCVのキャリブレーションはは180度以上の補正を扱えないので適当な無効値をいれる
const u = cx + fx * h * x / h_xy;
const v = cy + fy * h * y / h_xy;
uv[n * 2] = clamp(u, 0, 1);
uv[n * 2 + 1] = clamp(v, 0, 1);
}
return new THREE.BufferAttribute(uv, 2);
}
//テクスチャ座標のu,v=0or1は射影すべきデータが無い事ヲ示してるので当該ピクセルは表示しないshaderによるフィルタ
function filterInvalidUV_shader(material, tx, ty, tw, th) {
material.onBeforeCompile = (shader) => {
const { fragmentShader } = shader;
const pos = "#include <map_fragment>";
shader.fragmentShader = shader.fragmentShader.replace(pos, `
if(vMapUv.x<${tx + 0.01})discard;
if(vMapUv.y<${ty + 0.01})discard;
if(${tx + tw - 0.01}<vMapUv.x)discard;
if(${ty + th - 0.01}<vMapUv.y)discard;
${pos}`);
//console.log(shader.fragmentShader);
};
material.customProgramCacheKey = function () {
return `${tx}:${ty}:${tw}:${th}`;
}
}
function set_partial_texture(material, texture, tx, ty, tw, th) {
//処理対象はテクスチャの一部の領域に限定する
texture.repeat.set(tw, th);
texture.offset.set(tx, ty);
texture.colorSpace = THREE.DisplayP3ColorSpace;
material.map = texture;
}
function fisheyeMesh(tx, ty, tw, th) {
//skybox代わりの球を作成
const w_segs = 32 * 2;
const h_segs = 16 * 2;
//半球にするための最後Math.PI*0.5
let geometry = new THREE.SphereGeometry(100, w_segs, h_segs, 0, 2 * Math.PI, 0, Math.PI * 0.5);
//uv座標系を設定(シェーダのほうが綺麗だけど、uvならjsだけかける)
//UVマップはカメラが真上を向いているものとして作成される。
geometry.setAttribute('uv', fishUVbyPos(geometry.getAttribute("position")));
const material = new THREE.MeshBasicMaterial({
color: 0xffffff, side: THREE.DoubleSide,
fog: false, reflectivity: 0, combine: THREE.MixOperation,
});
//カメラ画像にない点は透明にするためのシェーダーフィルタ
filterInvalidUV_shader(material, tx, ty, tw, th);
return new THREE.Mesh(geometry, material);
}
class SubWindow {
constructor(name) {
this.name = "name";
this.window = window.open("", name, "menubar=no,toolbar=no,status=no,location=no");
this.window.document.close();
this.window.document.write('<html><style>html, body, #c1 {display: block;width: 100%;height: 100%;margin: 0;padding: 0;}</style><body><canvas id="c1" width="100%" height="100%"></canvas></body></html>');
this.canvas = this.window.document.getElementById("c1");
this.renderer = new THREE.WebGLRenderer({ canvas: this.canvas });
this.camera = new THREE.PerspectiveCamera(120, 16 / 9);
this.window.addEventListener("reeize", () => { this.update_size(); });
setTimeout(() => { this.update_size(); }, 1000)
}
update_size() {
const w = this.window.innerHeight;
const h = this.window.innerWidth;
this.camera.aspect = w / h;
this.camera.needUpdate = true;
this.renderer.setPixelRatio(this.window.devicePixelRatio);
this.renderer.setSize(h, w);
}
render(scene) {
this.renderer.render(scene, this.camera);
}
dispose() {
this.renderer.dispose()
}
}
//global sub window list
var sub_wins = [];
function launch_subwindow() {
sub_wins.forEach((sw) => { sw.dispose(); });
sub_wins = [];
sub_wins.push(new SubWindow("s1"));
sub_wins.push(new SubWindow("s2"));
sub_wins[1].camera.rotation.y = 0.5 * Math.PI;
sub_wins.push(new SubWindow("s3"));
sub_wins[2].camera.rotation.y = -0.5 * Math.PI;
}
//global 3D object
var fish1 = 0;
var fish2 = 0;
var main_plane = 0;
function init() {
//three.jsで表示するキャンバスサイズを指定
const width = 800;
const height = 600;
const renderer = new THREE.WebGLRenderer({
canvas: document.querySelector('#myCanvas')
});
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(width, height);
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(90, width / height);
const controls = new PointerLockControls(camera, renderer.domElement);
renderer.domElement.addEventListener(
'click',
function () {
controls.lock()
},
false
);
document.querySelector('#launch_sw').addEventListener(
'click', launch_subwindow, false
);
fish2 = fisheyeMesh(608 / 1920, 0, 608 / 1920, 1);
fish2.rotation.x = -Math.PI / 2; //デフォルトのカメラのむきを正面に変えておく
fish2.rotation.z = -Math.PI / 2; //デフォルトのカメラのむきを正面に変えておく
scene.add(fish2);
fish1 = fisheyeMesh(0, 0, 608 / 1920, 1);
fish1.rotation.x = -Math.PI / 2; //デフォルトのカメラのむきを正面に変えておく
fish1.rotation.z = Math.PI / 2; //デフォルトのカメラのむきを正面に変えておく
scene.add(fish1);
//正面カメラ用
const main_z = -5.0;
const main_deg = 115 / 2 / 180 * Math.PI;
const main_width = Math.abs(main_z) * 2 * Math.tan(main_deg) * 16 / Math.hypot(16, 9);
const main_height = main_width * 9 / 16;
main_plane = new THREE.Mesh(
new THREE.PlaneGeometry(main_height, main_width),
new THREE.MeshBasicMaterial({ color: 0xffffff })
);
main_plane.position.z = main_z;
main_plane.rotation.z = Math.PI / 2;
const main_group = new THREE.Group()
main_group.add(main_plane);
scene.add(main_group);
camera.position.z = 0;
camera.position.y = 0;
renderer.xr.setReferenceSpaceType("local-floor");
renderer.xr.enabled = true;
document.body.appendChild(VRButton.createButton(renderer));
const dummy_tex=new THREE.TextureLoader().load( "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAYAAAACCAYAAAB7Xa1eAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAfSURBVBhXY3gro/IfBJQ2+oBpe48zYJoBRGBKnvkPAJjrJSGwQKODAAAAAElFTkSuQmCC");
set_partial_texture(fish2.material, dummy_tex.clone(), 608 / 1920, 0, 608 / 1920, 1);
set_partial_texture(fish1.material, dummy_tex.clone(), 0, 0, 608 / 1920, 1);
set_partial_texture(main_plane.material, dummy_tex.clone(), 2 * 608 / 1920, 0, 608 / 1920, 1)
renderer.setAnimationLoop(function () {
renderer.render(scene, camera);
sub_wins.forEach((sw) => { sw.render(scene); });
});
window.set_video.addEventListener("click",set_video,false);
}
function set_video(){
init_video();
set_partial_texture(fish2.material, new THREE.VideoTexture(video), 608 / 1920, 0, 608 / 1920, 1);
set_partial_texture(fish1.material, new THREE.VideoTexture(video), 0, 0, 608 / 1920, 1);
set_partial_texture(main_plane.material, new THREE.VideoTexture(video), 2 * 608 / 1920, 0, 608 / 1920, 1)
}
window.addEventListener('DOMContentLoaded', init);
</script>
</head>
<style>
body {
background-color: #888;
}
</style>
<body>
<video id="video" style="width: 10px;height: auto;"></video><br>
<button id="launch_sw">lanunch sub window</button>
<button id="set_video">Set video</button><br>
<canvas id="myCanvas" style="width: 100vw;height: 90vh;"></canvas><br>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment