Last active
October 17, 2024 12:24
-
-
Save akirayou/e84398f74673e28a28949f23772d8efa to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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