【Threejs进阶教程-着色器篇】8. Shadertoy如何使用到Threejs - 基础版
- 前七篇地址,建议按顺序学习
- 致谢带我入门的[X01动力装甲]大佬
- 本文适用范围
- 怎么样在Shadertoy中画出正圆形
- shadertoy中的坐标系比例转换
- 理解Shadertoy的fragCoord
- 理解Shadertoy中的iResolution
- 转移Shadertoy 2D效果到片元着色器
- 处理mainImage和fragColor
- 全部源码
前七篇地址,建议按顺序学习
【Threejs进阶教程-着色器篇】1. Shader入门(ShadertoyShader和ThreejsShader入门)
【Threejs进阶教程-着色器篇】2. Uniform的基本用法与Uniform的调试
【Threejs进阶教程-着色器篇】3. Uniform的基本用法2与基本地球昼夜效果
【Threejs进阶教程-着色器篇】4. 2D SDF(一) SDF的基本用法
【Threejs进阶教程-着色器篇】5. 2D SDF(二)圆形波纹效果
【Threejs进阶教程-着色器篇】6. 2D SDF(三) 移动图形,限制图形,绘制多个图形
【Threejs进阶教程-着色器篇】7. 2D SDF 其他SDF介绍与图形边界绘制
致谢带我入门的[X01动力装甲]大佬
首先感谢X01动力装甲 大佬提供的文章
three.js使用Shadertoy的着色器
我的shader入门的一大半功劳,都要归功于X01动力装甲大佬的这篇文章,通过搬运Shadertoy,强化对片元着色器的理解
基本的Shadertoy到Three的使用,已经由装甲大佬讲清楚了,这里本文则是以个人理解的角度,对上述文章做一些补充式的教程
本文适用范围
大多数的2D Shadertoy的案例,到 Threejs的ShaderMaterial的片元着色器
注意是2D效果 到 片元着色器
怎么样在Shadertoy中画出正圆形
float sdCircle( in vec2 p, in float r )
{
return length(p)-r;
}
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
// Normalized pixel coordinates (from 0 to 1)
vec2 uv = fragCoord/iResolution.xy;
uv -= 0.5;//平移坐标系
float red = sdCircle(uv,0.1);//使用2d圆形sdf计算红色区域
red = red>0.0?0.0:1.0;//值大于0.0时取无色,否则取红色
// Output to screen
fragColor = vec4(red,0.0,0.0,1.0);
}
我们在Shadertoy中新建一个demo,然后使用sdCircle来绘制一个圆形,但是,我们发现,这里我们的圆形是一个椭圆,而在Threejs的片元着色器中,是一个正圆
这里的原因是,Shadertoy中,左侧的这个窗口,并不是一个正方形,而我们之前做的Threejs的片元着色器,是一个正方形plane,这里各位可以尝试修改一下之前的案例,把plane的宽高改成不相等,来看看实际效果
shadertoy中的坐标系比例转换
shadertoy的窗口是 800 * 450
所以我们可以直接用 uv.x *= 800/450或uv.y *= 450/800来让uv比例一致
虽然这样可以解决正圆的问题,但是我们却无法保证绘制的圆在中心了
我们需要一种更完美的方式,来重新计算uv
IQ大佬这里提供了一种算法
vec2 uv = (2.0*fragCoord-iResolution.xy)/iResolution.y;
这样我们绘制出来的圆就正常的放在了中心,大小上略有改变,但是整体影响不大,改成这样就可以与之前保持一致了
vec2 uv = (fragCoord-iResolution.xy/2.0)/iResolution.y;
因为IQ大佬的算法,是将整个画面处理成u = -1 ~1,v = -1 ~1 这个数值区间,而我们之前的算法,uv-=0.5的数值区间为 -0.5 ~ 0.5,所以当扩大一倍时,自然最终显示的图像就会缩小一倍
理解Shadertoy的fragCoord
我们可以另开一个ShadertoyDemo来试一下fragCoord
我们让uv 直接使用fragCoord的值,并且修改sdCircle的半径,这时我们发现,我们依然可以绘制出我们想要的图像,但是半径要达到300才行,依此类推,我们在半径450时,理论上来说,圆的上面会顶到窗口顶部
可以看出,刚好顶部就是圆形的切线
也就是说,在Shadertoy中,fragCoord,其实可以理解为每个画布上像素点的坐标
理解Shadertoy中的iResolution
在新建的Shadertoy的demo中,官方给出了这样的计算uv的公式
vec2 uv = fragCoord/iResolution.xy;
这样可以把整个Shadertoy的画布,变成咱们常规理解的uv的坐标系公式,就是从左到右,u的值从0变化到1,从下到上,v的值从0到1
基本上,通过上述公式推导,在shadertoy中,iResolution的xy值与fragCoord的xy值是相等的
这样我们再回头看IQ大佬的uv转换公式,
vec2 uv = (2.0 *fragCoord-iResolution.xy)/iResolution.y;
我们带入IQ大佬计算出来的最终坐标系的uv坐标的[0,0]点反推像素点位置
0 = (2.0 * x - 800) / 450 => x = 400
0 = (2.0 * y - 450) / 450 => y = 225
刚好得到的像素点坐标,就在屏幕的正中心处
转移Shadertoy 2D效果到片元着色器
使用iResolution和fragCoord方法的转移,在上面X01动力装甲大佬的文章中已经写明白了,这里我们主要讲,基于uv的一种代码转移方式
我们以IQ大佬的圆形SDF的案例为准,本篇暂不讲解此案例,仅讲述如何搬运
第一步,我们先观察能否做搬运
本次讲解的搬运方法,搬运Shadertoy的前提是:这个效果必须是2D的
另一个前提是,尽可能的找到比较干净的,使用的内置变量比较少的,没有交互的,shadertoy中有很多内置变量,如上图,如果使用了大量的内置变量,则这种的基本上很难直接搬运,必须要熟悉其代码后才可搬运
如上图中的IQ大佬的代码,他加入了iMouse,就是鼠标点击上去后,会生成一个圆的效果
这里我们先删掉,在后面的阶段中会讲解关于iMouse如何搬运的问题
删掉这部分代码后,变量m变成了无用变量,则变量m也可以随之删除
这里的p,其实就是uv
接下来,我们拿一个Shader的模板代码
【模板代码】用于编写Threejs Demo的模板代码
并直接将这里的代码全部复制到片元着色器中(注释可要可不要)
处理mainImage和fragColor
这两处我们在之前就已经讲过了,只需要把mainImage和fragColor,以及uv改成Threejs的命名即可
//旧代码
varying vec2 vUv;
float sdCircle( in vec2 p, in float r )
{
return length(p)-r;
}
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
vec2 p = (2.0*fragCoord-iResolution.xy)/iResolution.y;
float d = sdCircle(p,0.5);
// coloring
vec3 col = (d>0.0) ? vec3(0.9,0.6,0.3) : vec3(0.65,0.85,1.0);
col *= 1.0 - exp(-6.0*abs(d));
col *= 0.8 + 0.2*cos(150.0*d);
col = mix( col, vec3(1.0), 1.0-smoothstep(0.0,0.01,abs(d)) );
fragColor = vec4(col,1.0);
}
//新代码
varying vec2 vUv;
float sdCircle( in vec2 p, in float r )
{
return length(p)-r;
}
void main()
{
vec2 p = vUv;
float d = sdCircle(p,0.5);
// coloring
vec3 col = (d>0.0) ? vec3(0.9,0.6,0.3) : vec3(0.65,0.85,1.0);
col *= 1.0 - exp(-6.0*abs(d));
col *= 0.8 + 0.2*cos(150.0*d);
col = mix( col, vec3(1.0), 1.0-smoothstep(0.0,0.01,abs(d)) );
gl_FragColor = vec4(col,1.0);
}
Threejs中的最终效果
因为我们对uv的处理不同,所以这里我们也需要把uv挪动一下,挪动到我们需要的位置,直接p = vUv - 0.5即可
后面这个demo的效果各位可以改改参数玩一玩,
各位也可以自行尝试着去改一些Shadertoy效果到Threejs中
全部源码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<style>
body{
width:100vw;
height: 100vh;
overflow: hidden;
margin: 0;
padding: 0;
border: 0;
}
</style>
</head>
<body>
<script type="importmap">
{
"imports": {
"three": "../three/build/three.module.js",
"three/addons/": "../three/examples/jsm/"
}
}
</script>
<script type="x-shader/x-vertex" id="vertexShader">
varying vec2 vUv;
void main(){
vUv = vec2(uv.x,uv.y);
vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 );
gl_Position = projectionMatrix * mvPosition;
}
</script>
<script type="x-shader/x-fragment" id="fragmentShader">
varying vec2 vUv;
float sdCircle( in vec2 p, in float r )
{
return length(p)-r;
}
void main()
{
vec2 p = vUv - 0.5;
float d = sdCircle(p,0.5);
// coloring
vec3 col = (d>0.0) ? vec3(0.9,0.6,0.3) : vec3(0.65,0.85,1.0);
col *= 1.0 - exp(-6.0*abs(d));
col *= 0.8 + 0.2*cos(150.0*d);
col = mix( col, vec3(1.0), 1.0-smoothstep(0.0,0.01,abs(d)) );
gl_FragColor = vec4(col,1.0);
}
</script>
<script type="module">
import * as THREE from "../three/build/three.module.js";
import {OrbitControls} from "../three/examples/jsm/controls/OrbitControls.js";
window.addEventListener('load',e=>{
init();
addMesh();
render();
})
let scene,renderer,camera;
let orbit;
function init(){
scene = new THREE.Scene();
renderer = new THREE.WebGLRenderer({
alpha:true,
antialias:true
});
renderer.setSize(window.innerWidth,window.innerHeight);
document.body.appendChild(renderer.domElement);
camera = new THREE.PerspectiveCamera(50,window.innerWidth/window.innerHeight,0.1,2000);
camera.add(new THREE.PointLight());
camera.position.set(10,10,10);
scene.add(camera);
orbit = new OrbitControls(camera,renderer.domElement);
orbit.enableDamping = true;
scene.add(new THREE.GridHelper(10,10));
}
let uniforms = {
iTime:{value:0}
}
function addMesh() {
let geometry = new THREE.PlaneGeometry(10,10);
let material = new THREE.ShaderMaterial({
uniforms,
vertexShader:document.getElementById('vertexShader').textContent,
fragmentShader:document.getElementById('fragmentShader').textContent,
})
let mesh = new THREE.Mesh(geometry,material);
scene.add(mesh);
}
function render() {
renderer.render(scene,camera);
orbit.update();
requestAnimationFrame(render);
}
</script>
</body>
</html>