3D / 243

Environment reflection

周囲の景色を映す
反射方向から手続き的な環境色を参照し、球へ映り込む模様を描きます。写真の環境マップは使っていません。

見本の中を操作して変化を試してください。

ラボで詳しく試す ↗

ひとつ試すなら

反射の割合を変え、陰影と映り込みを見比べる。

こんなときに

立体の光、材質、反射の設計。

仕組みと使い方

反射の割合を変え、陰影と映り込みを見比べる。

3DWebGL光描画
実装コードを見る
<!doctype html><html lang="ja"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><style>body{margin:0;min-height:330px}.sample{width:100%;height:100%;min-height:330px;padding:12px!important;display:flex!important;flex-direction:column;justify-content:center;align-items:center;gap:8px!important;background:#f0efeb;color:#232323;font-family:Arial,Meiryo,sans-serif;overflow:hidden;box-sizing:border-box}.sample *{box-sizing:border-box}.lab-scene{width:100%;max-width:430px;height:140px;position:relative;overflow:hidden;border-bottom:1px solid #ccc}.lab-scene:has(canvas){max-width:320px;height:auto;aspect-ratio:16/9;flex-shrink:0}.lab-canvas{display:block;width:100%;height:100%!important;outline-offset:-3px}.lab-canvas:focus-visible{outline:2px solid #222}.lab-controls{width:100%;max-width:390px;display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px 14px}.lab-controls label{display:grid;grid-template-columns:1fr auto;gap:2px;font-size:11px;min-width:0}.lab-controls input{grid-column:1/-1;width:100%;min-width:0;margin:0;accent-color:#333;height:30px}.lab-controls output{font-variant-numeric:tabular-nums}.lab-actions{display:flex;gap:8px;align-items:center;flex-wrap:wrap;justify-content:center}.sample .lab-actions button{color:inherit;font:11px Arial,Meiryo,sans-serif;border:1px solid #aaa;border-radius:99px;background:transparent;min-height:44px;padding:8px 12px;cursor:pointer}.lab-status{font-size:10px;line-height:1.4;min-height:14px;margin:0;text-align:center}.lab-note{font-size:9px;line-height:1.3;color:#666;margin:0;text-align:center}.lab-actions button:focus-visible{outline:2px solid #222;outline-offset:2px}@media(prefers-reduced-motion:reduce){.sample *{animation-play-state:paused!important;transition:none!important}}</style><div class="sample"><div class="lab-scene"><canvas class="lab-canvas" role="img" aria-label="周囲の景色を映す"></canvas></div><div class="lab-controls"><label>環境の向き<output>1</output><input type="range" aria-label="環境の向き" min="-3.14" max="3.14" step="0.01" value="1" data-key="amount"></label><label>反射の割合 %<output>85</output><input type="range" aria-label="反射の割合 %" min="0" max="100" step="1" value="85" data-key="detail"></label></div><div class="lab-actions"><button data-play>停止</button><button data-reset>最初から</button></div><p class="lab-status" role="status"></p></div><script>const lab=(function runGpu(root,cfg,vs,fs,fallback){
 const canvas=root.querySelector('canvas'),status=root.querySelector('.lab-status'),play=root.querySelector('[data-play]'),reset=root.querySelector('[data-reset]'),abort=new AbortController(),mq=matchMedia('(prefers-reduced-motion:reduce)');
 const gl=canvas.getContext('webgl',{alpha:false,antialias:false}),values=Object.fromEntries(cfg.controls.map(c=>[c.key,c.value]));let program,buffer,shaders=[],uniforms={},frame=0,last=0,time=0,auto=!mq.matches,visible=true,dead=false,lost=false;
 const on=(el,event,fn)=>el.addEventListener(event,fn,{signal:abort.signal});
 if(!gl){abort.abort();return fallback(root,cfg);}
 function init(){shaders=[];for(const [type,src]of[[gl.VERTEX_SHADER,vs],[gl.FRAGMENT_SHADER,fs]]){const shader=gl.createShader(type);gl.shaderSource(shader,src);gl.compileShader(shader);if(!gl.getShaderParameter(shader,gl.COMPILE_STATUS))throw Error('Shader compilation failed');shaders.push(shader)}program=gl.createProgram();shaders.forEach(s=>gl.attachShader(program,s));gl.linkProgram(program);if(!gl.getProgramParameter(program,gl.LINK_STATUS))throw Error('Shader link failed');gl.useProgram(program);buffer=gl.createBuffer();gl.bindBuffer(gl.ARRAY_BUFFER,buffer);gl.bufferData(gl.ARRAY_BUFFER,new Float32Array([-1,-1,1,-1,-1,1,-1,1,1,-1,1,1]),gl.STATIC_DRAW);const p=gl.getAttribLocation(program,'position');gl.enableVertexAttribArray(p);gl.vertexAttribPointer(p,2,gl.FLOAT,false,0,0);for(const n of ['time','amount','detail','mode'])uniforms[n]=gl.getUniformLocation(program,n);canvas.width=640;canvas.height=360;gl.viewport(0,0,640,360);}
 function render(){if(dead||lost||!program)return;gl.useProgram(program);gl.uniform1f(uniforms.time,time);gl.uniform1f(uniforms.amount,values.amount);gl.uniform1f(uniforms.detail,values.detail??1);gl.uniform1f(uniforms.mode,cfg.mode);gl.drawArrays(gl.TRIANGLES,0,6);}
 function tick(now){frame=0;if(!auto||!visible||document.hidden||dead||lost)return;time+=Math.min(.05,(now-(last||now))/1000);last=now;render();frame=requestAnimationFrame(tick)}
 function sync(){cancelAnimationFrame(frame);frame=0;last=0;if(auto&&visible&&!document.hidden&&!dead&&!lost&&cfg.mode<2)frame=requestAnimationFrame(tick);play.textContent=auto?'停止':'再生';}
 on(play,'click',()=>{auto=!auto;sync()});on(reset,'click',()=>{time=0;render()});for(const input of root.querySelectorAll('[data-key]'))on(input,'input',()=>{values[input.dataset.key]=Number(input.value);input.closest('label').querySelector('output').textContent=input.value;render()});
 on(canvas,'webglcontextlost',e=>{e.preventDefault();lost=true;sync();status.textContent='描画を休止中です。コンテキストの復帰を待っています。'});on(canvas,'webglcontextrestored',()=>{lost=false;try{init();render();sync();status.textContent='WebGL / 描画を再開しました'}catch{status.textContent='描画を再開できませんでした。ページを再読み込みしてください。'}});
 on(document,'visibilitychange',sync);const observer=new IntersectionObserver(entries=>{visible=entries[0].isIntersecting;sync()});observer.observe(canvas);
 try{init();render();sync();status.textContent='WebGL / GLSL · 数値を変えて比較'}catch{status.textContent='この端末でシェーダーを実行できませんでした。';play.disabled=true;reset.disabled=true;}
 if(cfg.mode>=2)play.hidden=true;
 return{setAuto(on){auto=on&&!mq.matches;sync()},destroy(){dead=true;cancelAnimationFrame(frame);abort.abort();observer.disconnect();if(!lost){shaders.forEach(s=>gl.deleteShader(s));gl.deleteBuffer(buffer);gl.deleteProgram(program);gl.getExtension('WEBGL_lose_context')?.loseContext();}}};
})(document,{"mode":6,"controls":[{"key":"amount","label":"環境の向き","min":-3.14,"max":3.14,"value":1,"step":0.01},{"key":"detail","label":"反射の割合 %","min":0,"max":100,"value":85,"step":1}]},"attribute vec2 position;varying vec2 uv;void main(){uv=position*.5+.5;gl_Position=vec4(position,0.,1.);}","precision mediump float;varying vec2 uv;uniform float time;uniform float amount;uniform float detail;uniform float mode;\nfloat art(vec2 p){float disk=1.-smoothstep(.20,.21,length(p-vec2(.5)));float stripes=step(.5,fract((p.x+p.y)*9.));return mix(.17+.65*stripes,.48,disk);}\nvoid main(){vec2 p=uv;float g;vec3 color;\n if(mode<.5){vec2 d=p-.5;float r=length(d);p+=normalize(d+vec2(.0001))*sin(r*detail*30.-time*2.)*amount*.05;g=art(p);color=vec3(g);}\n else if(mode<1.5){p.x+=sin(p.y*45.+time*1.3)*sin(p.y*detail*7.-time)*amount*.05;g=art(p);color=vec3(g);}\n else if(mode<2.5){vec2 shift=(p-.5)*amount*.09;color=vec3(art(p+shift),art(p),art(p-shift));}\n else if(mode<3.5){float lum=uv.x;float bx=mod(floor(uv.x*160.),2.),by=mod(floor(uv.y*90.),2.);float threshold=(bx*2.+by+.5)/4.;float levels=max(2.,detail);g=floor(lum*(levels-1.)+threshold*amount)/(levels-1.);color=vec3(g);}\n else{vec2 xy=(uv-.5)*vec2(2.8,1.6);float rr=dot(xy,xy);if(rr>.49){color=vec3(.91);}else{vec3 n=normalize(vec3(xy,sqrt(.49-rr)));vec3 light=normalize(vec3(sin(amount)*2.,cos(amount)*1.3,1.2));float diffuse=max(0.,dot(n,light));float spec=pow(max(0.,dot(reflect(-light,n),vec3(0.,0.,1.))),max(2.,detail));float value=.12+.62*diffuse+.65*spec;if(mode>5.5){vec3 ray=reflect(vec3(0.,0.,-1.),n);float environment=.2+.65*smoothstep(-.3,.4,ray.y);float band=step(.65,sin(ray.x*8.+amount));value=mix(value,environment*(.65+.35*band),detail/100.);}color=vec3(value);}}\n gl_FragColor=vec4(color,1.);}",function runShaderFallback(root,cfg){
 const canvas=root.querySelector('canvas'),ctx=canvas.getContext('2d'),status=root.querySelector('.lab-status'),play=root.querySelector('[data-play]'),reset=root.querySelector('[data-reset]'),abort=new AbortController(),mq=matchMedia('(prefers-reduced-motion:reduce)');
 canvas.width=160;canvas.height=90;const pixels=ctx.createImageData(160,90),values=Object.fromEntries(cfg.controls.map(c=>[c.key,c.value]));let frame=0,time=0,last=0,auto=!mq.matches,visible=true,dead=false;
 const clamp=x=>Math.max(0,Math.min(1,x)),smooth=(a,b,x)=>{const t=clamp((x-a)/(b-a));return t*t*(3-2*t)},fract=x=>x-Math.floor(x),art=(x,y)=>{const disk=1-smooth(.2,.21,Math.hypot(x-.5,y-.5)),stripes=fract((x+y)*9)>=.5?1:0;return(.17+.65*stripes)*(1-disk)+.48*disk};
 function draw(){const amount=values.amount,detail=values.detail??1,mode=cfg.mode;for(let y=0;y<90;y++)for(let x=0;x<160;x++){let u=x/160,v=1-y/90,r,g,b;if(mode<2){if(mode===0){const dx=u-.5,dy=v-.5,len=Math.hypot(dx,dy),shift=Math.sin(len*detail*30-time*2)*amount*.05;u+=dx/Math.max(.0001,len)*shift;v+=dy/Math.max(.0001,len)*shift}else u+=Math.sin(v*45+time*1.3)*Math.sin(v*detail*7-time)*amount*.05;r=g=b=art(u,v)}else if(mode===2){const dx=(u-.5)*amount*.09,dy=(v-.5)*amount*.09;r=art(u+dx,v+dy);g=art(u,v);b=art(u-dx,v-dy)}else if(mode===3){const threshold=((x%2)*2+((89-y)%2)+.5)/4;r=g=b=Math.floor(u*(detail-1)+threshold*amount)/(detail-1)}else{const xx=(u-.5)*2.8,yy=(v-.5)*1.6,rr=xx*xx+yy*yy;if(rr>.49)r=g=b=.91;else{const nx=xx/.7,ny=yy/.7,nz=Math.sqrt(.49-rr)/.7,lx=Math.sin(amount)*2,ly=Math.cos(amount)*1.3,lz=1.2,ll=Math.hypot(lx,ly,lz),dot=nx*lx/ll+ny*ly/ll+nz*lz/ll,diff=Math.max(0,dot),spec=Math.pow(Math.max(0,2*dot*nz-lz/ll),Math.max(2,detail));r=g=b=.12+.62*diff+.65*spec;if(mode>5){const rx=2*nz*nx,ry=2*nz*ny,env=.2+.65*smooth(-.3,.4,ry),band=Math.sin(rx*8+amount)>=.65?1:0;r=g=b=r*(1-detail/100)+env*(.65+.35*band)*detail/100}}}const i=(y*160+x)*4;pixels.data[i]=clamp(r)*255;pixels.data[i+1]=clamp(g)*255;pixels.data[i+2]=clamp(b)*255;pixels.data[i+3]=255;}ctx.putImageData(pixels,0,0);}
 function tick(now){frame=0;if(!auto||!visible||document.hidden||dead)return;if(now-last>40){time+=Math.min(.1,(now-(last||now))/1000);last=now;draw()}frame=requestAnimationFrame(tick)}
 function sync(){cancelAnimationFrame(frame);last=0;if(auto&&visible&&!document.hidden&&cfg.mode<2&&!dead)frame=requestAnimationFrame(tick);play.textContent=auto?'停止':'再生';}
 const on=(el,event,fn)=>el.addEventListener(event,fn,{signal:abort.signal});
 on(play,'click',()=>{auto=!auto;sync()});on(reset,'click',()=>{time=0;draw()});for(const field of root.querySelectorAll('[data-key]'))on(field,'input',()=>{values[field.dataset.key]=Number(field.value);field.closest('label').querySelector('output').textContent=field.value;draw()});on(document,'visibilitychange',sync);
 const observer=new IntersectionObserver(entries=>{visible=entries[0].isIntersecting;sync()});observer.observe(canvas);status.textContent='Canvasで代替描画中 · この環境はWebGLに非対応';if(cfg.mode>=2)play.hidden=true;draw();sync();return{setAuto(on){auto=on&&!mq.matches;sync()},destroy(){dead=true;cancelAnimationFrame(frame);abort.abort();observer.disconnect()}};
});addEventListener('pagehide',()=>lab.destroy(),{once:true});</script></html>