レイマーチングを試したくなりました。
ポイントは距離関数とかをいろいろ変化させて様子を見てみたいのですが、そのためにややこしいシェーダーの仕様とか気にしたくありません。
僕は距離関数だけに集中したい。
そこで、THREE.jsのサンプル
https://threejs.org/examples/webgl_raymarching_reflect.html
https://qiita.com/gam0022/items/03699a07e4a4b5f2d41f
を改造して、あとは
float sceneDist(vec3 p){
return pからの距離
}
を書き換えるだけでよいとしたいと思います。
半分くらい自分用メモ。
主な仕様
- 単一HTMLファイル(除 THREE.jsなどのjsファイル)
- 特に時間変化や光源の変化は見ない
- マウスドラッグで見る向きを変えられる
結果
書き換えるべき距離関数は50行目くらいにあります。
<!DOCTYPE html>
<html lang="en">
<head>
<title>three.js webgl - raymarching - reflect</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
<style type="text/css">
body {
background-color: black;
margin: 0;
padding: 0;
}
a { color: skyblue }
canvas {
display: block;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
margin: auto;
}
#info {
color: white;
font-size: 13px;
position: absolute;
bottom: 10px;
width: 100%;
text-align: center;
z-index: 100;
}
</style>
</head>
<body>
<script id="fragment_shader" type="x-shader/x-fragment">
precision highp float;
uniform vec2 resolution;
uniform mat4 viewMatrix;
uniform vec3 cameraPosition;
uniform mat4 cameraWorldMatrix;
uniform mat4 cameraProjectionMatrixInverse;
const float EPS = 0.01;
const float OFFSET = EPS * 100.0;
const vec3 lightDir = vec3( -0.48666426339228763, 0.8111071056538127, -0.3244428422615251 );
// distance functions
float sceneDist( vec3 p ) {
return length(p)-1.0;
}
vec3 sceneColor( vec3 p ) {
return vec3( 0.0, 1.0, 0.0 );
}
vec3 getNormal( vec3 p ) {
return p;
}
float getShadow( vec3 ro, vec3 rd ) {
float h = 0.0;
float c = 0.0;
float r = 1.0;
float shadowCoef = 0.5;
for ( float t = 0.0; t< 50.0; t++ ) {
h = sceneDist( ro + rd * c );
if ( h < EPS ) return shadowCoef;
r = min( r, h * 16.0 / (c + EPS));
c += h;
}
return 1.0 - shadowCoef + r * shadowCoef;
}
vec3 getRayColor( vec3 origin, vec3 ray, out vec3 pos, out vec3 normal, out bool hit ) {
// marching loop
float dist;
float depth = 0.0;
pos = origin;
for ( int i = 0; i < 64; i++ ){
dist = sceneDist( pos );
depth += dist;
pos = origin + depth * ray;
if ( abs(dist) < EPS ) break;
}
// hit check and calc color
vec3 color;
if ( abs(dist) < EPS ) {
normal = getNormal( pos );
float diffuse = clamp( dot( lightDir, normal ), 0.1, 1.0 );
float specular = pow( clamp( dot( reflect( lightDir, normal ), ray ), 0.0, 1.0 ), 10.0 );
float shadow = getShadow( pos + normal * OFFSET, lightDir );
color = ( sceneColor( pos ) + vec3( 0.8 ) * specular ) * max( 0.5, shadow );
hit = true;
} else {
color = vec3( 0.4 );
}
return color - pow( clamp( 0.05 * depth, 0.0, 0.6 ), 2.0 ) * 0.1;
}
void main(void) {
// screen position
vec2 screenPos = ( gl_FragCoord.xy * 2.0 - resolution ) / min( resolution.x, resolution.y );
// convert ray direction from screen coordinate to world coordinate
vec3 ray = (cameraWorldMatrix * cameraProjectionMatrixInverse * vec4( screenPos.xy, 1.0, 1.0 )).xyz;
ray = normalize( ray );
// camera position
vec3 cPos = cameraPosition;
// cast ray
vec3 color = vec3( 0.0 );
vec3 pos, normal;
bool hit;
float alpha = 1.0;
for ( int i = 0; i < 3; i++ ) {
color += alpha * getRayColor( cPos, ray, pos, normal, hit );
alpha *= 0.3;
ray = normalize( reflect( ray, normal ) );
cPos = pos + normal * OFFSET;
if ( !hit ) break;
}
gl_FragColor = vec4( color, 1.0 );
}
</script>
<script id="vertex_shader" type="x-shader/x-vertex">
attribute vec3 position;
void main(void) {
gl_Position = vec4(position, 1.0);
}
</script>
<script src="js/three.min.js"></script>
<script src="js/controls/FlyControls.js"></script>
<script src="js/libs/stats.min.js"></script>
<script src="js/libs/dat.gui.min.js"></script>
<script>
var camera, scene, controls, renderer;
var geometry, material, mesh;
var mouse = new THREE.Vector2( 0.5, 0.5 );
var canvas;
var stats;
var rotation_function = function(x, y){};
var clock = new THREE.Clock();
function camera_init(){
camera.position.set(0.0, 0.0, 2.0);
camera.up.set(0.0, 1.0, 0.0);
camera.lookAt( new THREE.Vector3( 0.0, 0.0, 0.0 ));
}
var config = {
saveImage: function() {
renderer.render( scene, camera );
window.open( canvas.toDataURL() );
},
resolution: '512',
cameraReset: camera_init,
};
init();
render();
function init() {
renderer = new THREE.WebGLRenderer();
renderer.setPixelRatio( window.devicePixelRatio );
renderer.setSize( config.resolution, config.resolution );
canvas = renderer.domElement;
canvas.addEventListener( 'mousedown', onMouseDown, false );
canvas.addEventListener( 'mouseup', onMouseUp, false );
canvas.addEventListener( 'mouseout', function(e){onMouseUp(e);}, false );
canvas.addEventListener( 'mousemove', onMouseMove, false );
canvas.addEventListener( 'mousewheel', onMouseWheel, false);
window.addEventListener( 'resize', onWindowResize );
document.body.appendChild( canvas );
// Scene
scene = new THREE.Scene();
camera = new THREE.PerspectiveCamera( 60, canvas.width / canvas.height, 1, 2000 );
camera.lookAt( new THREE.Vector3( 0.0, 0.0, 0.0 ) );
geometry = new THREE.PlaneBufferGeometry( 2.0, 2.0 );
console.log(document.getElementById( 'fragment_shader' ).textContent);
material = new THREE.RawShaderMaterial( {
uniforms: {
resolution: { value: new THREE.Vector2( canvas.width, canvas.height ) },
cameraWorldMatrix: { value: camera.matrixWorld },
cameraProjectionMatrixInverse: { value: new THREE.Matrix4().getInverse( camera.projectionMatrix ) }
},
vertexShader: document.getElementById( 'vertex_shader' ).textContent,
fragmentShader: document.getElementById( 'fragment_shader' ).textContent
} );
mesh = new THREE.Mesh( geometry, material );
mesh.frustumCulled = false;
scene.add( mesh );
// Controls
controls = new THREE.FlyControls( camera, canvas );
controls.autoForward = true;
controls.dragToLook = false;
controls.rollSpeed = Math.PI / 12;
controls.movementSpeed = 0.5;
// GUI
var gui = new dat.GUI();
gui.add( config, 'saveImage' ).name( 'Save Image' );
gui.add( config, 'resolution', [ '256', '512', '800', 'full' ] ).name( 'Resolution' ).onChange( function( value ) {
if ( value !== 'full' ) {
canvas.width = value;
canvas.height = value;
}
onWindowResize();
} );
gui.add( config, 'cameraReset').name("Reset Camera");
stats = new Stats();
document.body.appendChild( stats.domElement );
camera_init();
}
function render( timestamp ) {
var delta = clock.getDelta();
stats.begin();
material.uniforms.resolution.value.set( canvas.width, canvas.height );
material.uniforms.cameraProjectionMatrixInverse.value.getInverse( camera.projectionMatrix );
renderer.render( scene, camera );
stats.end();
requestAnimationFrame( render );
}
var rotation_horizon = function(angle){
var r = camera.position.length();
right_dir = THREE.Vector3.prototype.crossVectors(camera.position, camera.up).normalize();
camera.position.addScaledVector(right_dir, r*angle/2.0);
camera.position.setLength(r);
right_dir.cross(camera.up).normalize();
}
function rotation_vertical(angle){
var r = camera.position.length();
camera.position.addScaledVector(camera.up, r*angle/2.0);
camera.position.setLength(r);
right_dir = THREE.Vector3.prototype.crossVectors(camera.position, camera.up).normalize();
camera.up.set(right_dir.x, right_dir.y, right_dir.z);
camera.up.cross(camera.position).normalize();
}
function onMouseDown( event ) {
rotation_function = function(x, y){
rot_speed = 10.0;// controls.rollSpeed;
rotation_horizon(x*rot_speed);
rotation_vertical(y*rot_speed);
camera.lookAt( new THREE.Vector3( 0.0, 0.0, 0.0 ));
}
}
function onMouseMove( event ) {
px = mouse.x;
py = mouse.y;
mouse.x = event.clientX / canvas.width;
mouse.y = event.clientY / canvas.height;
rotation_function(mouse.x - px, mouse.y - py);
}
function onMouseUp( event ) {
rotation_function = function(x, y){};
}
var delta = 0.05;
function onMouseWheel( event ){
if(event.wheelDelta > 0){
camera.position.multiplyScalar(1. + delta);
}else{
camera.position.multiplyScalar(1. - delta);
}
}
function onWindowResize( e ) {
if ( config.resolution === 'full' ) {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
}
renderer.setSize( canvas.width, canvas.height );
}
</script>
</body>
</html>
これに、
https://github.com/gam0022/webgl-sandbox/tree/master/js
このjsフォルダを同じディレクトリに置いてブラウザで見ると、
緑色の球が1個あるだけのレンダリングが実行されるはずです。
ファイル名は何でもいいです。
ソースコード解説
距離関数
最小では
float sceneDist( vec3 p ) {
return length(p) - 1.0;
}
の部分を好きなものにするだけで大丈夫かと思います。
これで返すべきは、現在の光線の先端 p
からもっとも近い物体までの直線距離です。
一応法線は円のものになるようにしていますが、光の反射がおかしくなるだけで物体のシルエットを知るには十分です。
色
色も変えてみたいという場合は、この関数を変えてやります。
vec3 sceneColor( vec3 p ) {
return vec3( 0.0, 1.0, 0.0 );
}
座標p
における物体の色を定義してやります。
物体が存在していない座標も定義できてしまいますが、レイマーチングがちゃんとしていれば関係ないはずです。
法線
いや法線もしっかり試したいという人は、こちらも編集してください。
vec3 getNormal( vec3 p ) {
return p;
}
これは距離関数がちゃんとしていたら光線が最後にぶち当たった物体表面です(。
結局この関数には、物体表面の法線ベクトルを座標の関数として書いたものを置いてやればよいです。
物体表面で無い座標では・・・正直分かりませんが、近場の表面とするか、ベクトル場としてしまうか、いっそ0ベクトルを返しておけばいいと思います。マーチングがちゃんと収束すれば関係ないはずですし。
その他サンプルからの変更点
詳細は省きますが、サンプルに比べて明るくしました。
ここらへんの光の調整をGLSLによるコーディングで試せるようにするのは次の段階で今回は過剰だと考え、スルーします。
視点変更
レンダリング関数の中で可変変数になっているのが世界座標系ではなくカメラ座標と向きなので、マウスドラッグで動くのはカメラの方とします。
水平方向
var rotation_horizon = function(angle){
var r = camera.position.length();
right_dir = THREE.Vector3.prototype.crossVectors(camera.position, camera.up).normalize();
camera.position.addScaledVector(right_dir, r*angle/2.0);
camera.position.setLength(r);
right_dir.cross(camera.up).normalize();
}
垂直方向
function rotation_vertical(angle){
var r = camera.position.length();
camera.position.addScaledVector(camera.up, r*angle/2.0);
camera.position.setLength(r);
right_dir = THREE.Vector3.prototype.crossVectors(camera.position, camera.up).normalize();
camera.up.set(right_dir.x, right_dir.y, right_dir.z);
camera.up.cross(camera.position).normalize();
}
マウス押下
ドラッグするとはつまりマウスボタンを押した状態でマウスが動くこと。
その状態をマウスが動いた時の関数を切り替えることで実現します。
function onMouseDown( event ) {
rotation_function = function(x, y){
rot_speed = 10.0;// controls.rollSpeed;
rotation_horizon(x*rot_speed);
rotation_vertical(y*rot_speed);
camera.lookAt( new THREE.Vector3( 0.0, 0.0, 0.0 ));
}
}
rot_speed
でいい感じにドラッグしているように感じる速度になるように調整します。
マウスが動いた時のイベントでrotation_function
が呼び出されます。
function onMouseMove( event ) {
px = mouse.x;
py = mouse.y;
mouse.x = event.clientX / canvas.width;
mouse.y = event.clientY / canvas.height;
rotation_function(mouse.x - px, mouse.y - py);
}
mouse
は直前のマウス座標を保持します。
ドラッグ終了
function onMouseUp( event ) {
rotation_function = function(x, y){};
}
ボタンを離した時、カメラ回転関数を何もしない関数に変更します。
拡大縮小
var delta = 0.05;
function onMouseWheel( event ){
if(event.wheelDelta > 0){
camera.position.multiplyScalar(1. + delta);
}else{
camera.position.multiplyScalar(1. - delta);
}
}
マウスホイールが起こると定数倍拡大・縮小されるようにします。
イベント登録
canvas.addEventListener( 'mousedown', onMouseDown, false );
canvas.addEventListener( 'mouseup', onMouseUp, false );
canvas.addEventListener( 'mouseout', function(e){onMouseUp(e);}, false );
canvas.addEventListener( 'mousemove', onMouseMove, false );
canvas.addEventListener( 'mousewheel', onMouseWheel, false);
ドラッグ開始はマウスダウン、ドラッグ終了はマウスアップもしくはCanvasからマウスが離れたとき。ということで、mouseout
のイベントはmouseup
のものを流用します。
カメラリセットボタン
右上に何気にあるメニューにカメラを初期状態に戻すボタンを追加します。
- カメラを初期状態に戻す関数
function camera_init(){
camera.position.set(0.0, 0.0, 2.0);
camera.up.set(0.0, 1.0, 0.0);
camera.lookAt( new THREE.Vector3( 0.0, 0.0, 0.0 ));
}
- メニュー設定
var config = {
saveImage: function() {中略},
resolution: '512',
cameraReset: camera_init,
};
- メニュー反映
// GUI
var gui = new dat.GUI();
(中略)
gui.add( config, 'cameraReset').name("Reset Camera");
実例
距離関数をトーラスにしてみます。
参考: http://iquilezles.org/www/articles/distfunctions/distfunctions.htm
const vec3 t = vec3(1.0, 0.2, 0.0); // radian, width, 0
float sceneDist( vec3 p ) {
vec2 q = vec2(length(p.xz)-t.x,p.y);
return length(q)-t.y;
}
法線が円のままなので光り方がおかしいですね。
物体をグリグリ回すのと拡大縮小、リセットが機能しているのが確認できました。
GIFアニメではあまりカーソルに追随していないように見えますが、個人的には満足しています。
もし余裕があるなら
- 光源の向きなどを変えられるように
- 時間変数を追加して時間で変化する物体に対応
をしたい気もします。
その他
単一ファイルにHTML、CSS、JavaScript、glslの4つの言語があるんですが。
もしこれをphpなどで出力しようとするとその言語も混ざるのでしょうか。