Uniform的基本用法2
- 关于本Shader教程
- 前两篇地址,请按顺序学习
- 本篇使用到的资源
- 用uniform传递纹理
- 代码分析
- texture类型的uniform
- 在shader中接收uniform
- texture2D()
- 处理图片压缩
- 修改wrapS和wrapT
- 切换成夜景
- 效果切换
- Mix()
- 昼夜切换升级
- 改动代码
- 效果分析
- 解决球体分界线太过明显的问题
- 让昼夜动起来
- 改动代码
- 最终效果
- 案例完整源码
- 如有不明白的,可以在下方留言或者加群
关于本Shader教程
- 本教程着重讲解Shadertoy的shader和Threejs的Shader,与原生WebGLShader略有不同,如果需要学习原生WebGL的shader,请参考《WebGL编程指南》
- 本人的shader水平也比较基础,文章中所写代码,不一定是最佳的代码,思路也不一定是最好的思路,所以一切本人的Shader教程下,所有的代码及思路以及学习建议均仅供参考,且目前本教程可能不适用于WebGPU,如果有大佬路过看到本人文章,觉得有可以指点之处,可以在下面留言,我们一起进步
- 数学水平不行的人,尤其是高中数学都及格不了的,不建议入坑Shader
- 本教程会在讲解片元着色器时,使用Shadertoy来编写demo,所以教程中会出现一部分Shadertoy的代码
- 本段内容将会出现在本人所有的【进阶教程-着色器篇】的文章中
前两篇地址,请按顺序学习
【Threejs进阶教程-着色器篇】1. Shader入门(ShadertoyShader和ThreejsShader入门)
【Threejs进阶教程-着色器篇】2. Uniform的基本用法与Uniform的调试
本篇使用到的资源
threejs开发包中
three/examples/textures/plantes/earth_atmos_2048.jpg 注意这张图片后缀是jpg!
three/examples/textures/plantes/earth_lights_2048.png
three/examples/textures/transition/transition5.png
用uniform传递纹理
有些时候不是说所有的图形效果都需要用数学去实现,还可以使用贴图
<!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;
uniform sampler2D uDiffuse1;
void main(){
vec4 col = texture2D(uDiffuse1,vUv);
gl_FragColor = col;
}
</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(15,15,15);
scene.add(camera);
orbit = new OrbitControls(camera,renderer.domElement);
orbit.enableDamping = true;
scene.add(new THREE.GridHelper(10,10));
}
let uniforms = {
uDiffuse1:{value:null}
}
function addMesh() {
let textureLoader = new THREE.TextureLoader();
uniforms.uDiffuse1.value = textureLoader.load('./earth_atmos_2048.jpg');
let geometry = new THREE.SphereGeometry(5,32,32);
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>
代码效果
代码分析
texture类型的uniform
shader允许传递一个texture类型的对象到uniform,这里在threejs中,对应的是 THREE.Texture类型的对象,也就是TexthreLoader读取出来的图片,并转换成的texture实例
注意,一般情况下,图片的使用要考虑异步,本地化的读取效率非常高,不会出现渲染延迟,线上的话,可能会让地球变成一个黑色或者白色的球体
这个uniform的写法有两种,一种是像上面一样先赋值null,然后再读取,另一种是在读取的时候创建uniform的key,任选一个自己喜欢的风格即可,没有太多的要求,只是注意,给uniform赋值要这样写
//如果采用第二种写法,可以这样写
uniforms.uDiffuse1 = {value:texture}
在shader中接收uniform
varying vec2 vUv;
uniform sampler2D uDiffuse1; //这里在shader中,对应sampler2D这个类型
void main(){
vec4 col = texture2D(uDiffuse1,vUv); // 对图片逐uv取色
gl_FragColor = col;
}
texture2D()
texture2D函数的参数有两个,第一个是一个vec2的对象,第二个是一个sampler2D类型的数据
一般前者我们都使用uv,这个就是最基本的贴图代码,读取图片的uv并将颜色给到指定uv的顶点处
我们把几何体换成正方向平面,这样看的更明显一些
可以看出,在正方形平面的贴图上,图片出现了压缩,这是因为图片本身并不是一个正方形,但是以逐uv的形式来读取了这张图片,所以最终造成了压缩的问题
处理图片压缩
这里我们在shader中,把vUv.x 放大即可
varying vec2 vUv;
uniform sampler2D uDiffuse1;
void main(){
vec2 uUv = vec2(vUv.x / 2.0, vUv.y);
vec4 col = texture2D(uDiffuse1,uUv);
gl_FragColor = col;
}
我们把uv.x除以2.0,这样我们就只加载 0 < uv.x < 0.5范围内的图片, 0< uv.y < 1的图片,所以我们可以看到,y轴没有变化,而x轴拉回去了
我们也可以继续修改,看看对uv的xy都乘或除2.0的效果怎么样
发现新的问题了,我们在除以2.0的情况下,我们只取了图片的左下角,但是乘以2.0的时候,并不如我们想的结果那样,平铺四张图
所以这里我们要对纹理做一下处理
修改wrapS和wrapT
uniforms.uDiffuse1.value = textureLoader.load('./earth_atmos_2048.jpg');
uniforms.uDiffuse1.value.wrapT = THREE.RepeatWrapping;
uniforms.uDiffuse1.value.wrapS = THREE.RepeatWrapping;
varying vec2 vUv;
uniform sampler2D uDiffuse1;
void main(){
vec2 uUv = vec2(vUv.x * 2.0, vUv.y * 2.0);
vec4 col = texture2D(uDiffuse1,uUv);
gl_FragColor = col;
}
这样,我们就解决了没有平铺图片的问题,这是一种通过shader的方式,来改变图片平铺方式的解决办法
切换成夜景
首先我们引入第二张图片,并改回球体和uv,并额外添加一个 uChange属性
修改代码
let uniforms = {
uDiffuse1:{value:null},
uDiffuse2:{value:null},
uChange:{value:0.0}
}
function addMesh() {
let textureLoader = new THREE.TextureLoader();
uniforms.uDiffuse1.value = textureLoader.load('./earth_atmos_2048.jpg');
uniforms.uDiffuse1.value.wrapT = THREE.RepeatWrapping;
uniforms.uDiffuse1.value.wrapS = THREE.RepeatWrapping;
uniforms.uDiffuse2.value = textureLoader.load('./earth_lights_2048.png');
uniforms.uDiffuse2.value.wrapT = THREE.RepeatWrapping;
uniforms.uDiffuse2.value.wrapS = THREE.RepeatWrapping;
let geometry = new THREE.SphereGeometry(5,32,32);
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);
let gui = new GUI();
gui.add(uniforms.uChange,'value',0,1).name('渐变');
}
片元着色器代码
varying vec2 vUv;
uniform sampler2D uDiffuse1;
uniform sampler2D uDiffuse2;
uniform float uChange;
void main(){
vec4 col = texture2D(uDiffuse2,vUv);
gl_FragColor = col;
}
效果切换
我们现在要做的是,如果uChange = 1 ,则显示白天的贴图,如果uChange = 0,则显示夜晚贴图
varying vec2 vUv;
uniform sampler2D uDiffuse1;
uniform sampler2D uDiffuse2;
uniform float uChange;
void main(){
vec4 col1 = texture2D(uDiffuse1,vUv);
vec4 col2 = texture2D(uDiffuse2,vUv);
gl_FragColor = vec4(
col1.r * uChange + col2.r * (1.0 - uChange),
col1.g * uChange + col2.g * (1.0 - uChange),
col1.b * uChange + col2.b * (1.0 - uChange),
col1.a * uChange + col2.a * (1.0 - uChange)
);
}
我们这样想,既然change = 0.1的时候,那么此时图片1的颜色值为最淡,然后图片2的颜色值为最深,对应rgb三个颜色都是这样的结果
Mix()
但是其实,这个算法,官方早就给你想好了,我们只需使用mix即可,下面两种写法的效果完全一致
//旧代码
gl_FragColor = vec4(
col1.r * uChange + col2.r * (1.0 - uChange),
col1.g * uChange + col2.g * (1.0 - uChange),
col1.b * uChange + col2.b * (1.0 - uChange),
col1.a * uChange + col2.a * (1.0 - uChange)
);
//新代码
gl_FragColor = mix(col2,col1,uChange);
mix是个非常非常常用的函数,主要用来线性混合数据,mix可以适用于很多种类型的数据,也有很多种用法,后续会经常用到和提到mix
昼夜切换升级
细心的朋友注意到了,这个切换效果,是全地球一起在切换,而并不是在模拟日出日落的那种昼夜切换,所以这个时候我们就要用到特殊的一种混合模式,这里我们加载第三张图,且拿掉uChange这个uniform
改动代码
我们加入第三张贴图,且加入时间变量iTime
由于在shadertoy中的时间变量也是iTime,所以后续所有的教程中,出现的时间变量都会命名为iTime
let uniforms = {
uDiffuse1:{value:null},
uDiffuse2:{value:null},
uChangeTexture:{value:null},
iTime:{value:0.01}
}
function addMesh() {
let textureLoader = new THREE.TextureLoader();
uniforms.uDiffuse1.value = textureLoader.load('./earth_atmos_2048.jpg');
uniforms.uDiffuse1.value.wrapT = THREE.RepeatWrapping;
uniforms.uDiffuse1.value.wrapS = THREE.RepeatWrapping;
uniforms.uDiffuse2.value = textureLoader.load('./earth_lights_2048.png');
uniforms.uDiffuse2.value.wrapT = THREE.RepeatWrapping;
uniforms.uDiffuse2.value.wrapS = THREE.RepeatWrapping;
uniforms.uChangeTexture.value = textureLoader.load('./transition5.png');
uniforms.uChangeTexture.value.wrapT = THREE.RepeatWrapping;
uniforms.uChangeTexture.value.wrapS = THREE.RepeatWrapping;
let geometry = new THREE.SphereGeometry(5,32,32);
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);
uniforms.iTime.value += 0.01;
}
片元着色器改动
varying vec2 vUv;
uniform sampler2D uDiffuse1;
uniform sampler2D uDiffuse2;
uniform sampler2D uChangeTexture;
void main(){
vec4 col1 = texture2D(uDiffuse1,vUv);
vec4 col2 = texture2D(uDiffuse2,vUv);
vec4 col3 = texture2D(uChangeTexture,vUv);
gl_FragColor = mix(col2,col1,col3.r);
}
效果分析
这样,我们看到的效果,就是一边是白天,一边是黑夜的效果了
我们切换回plane,来看看效果演变
可以看出,最黑的地方,最终使用的是夜晚的图片,也就是说此处的值,r值是最低的
最白的地方,最终使用的是白天的图片,也就是说此处的值,r值是最高的
中间的部分,随着上图的颜色变化而变化,白色越强的地方,白天图片的强度越高
黑色越强的地方,黑夜图片的强度越高
解决球体分界线太过明显的问题
但是这里有个很明显的问题,就是应用到球体上之后,左右两侧的颜色差距太大,导致了明显的分界线
我们切换回球体,然后对uv做一下处理
首先,我们的图片,是左边黑右边白,那么我们试着移动一下图片的像素,给uv.x - 0.5
此时图片会变成下面圈出来的这一块
然后,接下来,我们让负数变正数,则左边的这一块变成了,对abs(uv.x - 0.5)
原先按照正常的取值流程,红框的最左边是-0.5,但是我们给它变成正的了,所以后面会产生镜像效果
但是这一块黑色区域太大,所以我们要把最终结果再乘2,让图片截取到完全白色的区域
此时,uv.x的取值范围,就变成了 -1 ~1,就变成了上面的图片效果
然后我们带入到代码中试一下
<script type="x-shader/x-fragment" id="fragmentShader">
varying vec2 vUv;
uniform sampler2D uDiffuse1;
uniform sampler2D uDiffuse2;
uniform sampler2D uChangeTexture;
void main(){
vec4 col1 = texture2D(uDiffuse1,vUv);
vec4 col2 = texture2D(uDiffuse2,vUv);
//对vUv.x - 0.5然后绝对值,再乘2
vec4 col3 = texture2D(uChangeTexture, vec2(abs(vUv.x - 0.5) * 2.0,vUv.y));
gl_FragColor = mix(col2,col1,col3.r);
}
</script>
让昼夜动起来
改动代码
varying vec2 vUv;
uniform sampler2D uDiffuse1;
uniform sampler2D uDiffuse2;
uniform sampler2D uChangeTexture;
uniform float iTime;
void main(){
vec4 col1 = texture2D(uDiffuse1,vec2(vUv.x + iTime,vUv.y));
vec4 col2 = texture2D(uDiffuse2,vec2(vUv.x + iTime,vUv.y));
vec4 col3 = texture2D(uChangeTexture,vec2(abs(vUv.x - 0.5) * 2.0,vUv.y));
gl_FragColor = mix(col2,col1,col3.r);
}
由于col3中的vUv.x已经做了太多处理了,所以我们把跟随时间动的代码,放到了前面两张图上,让前面两张图动起来
然后发现动的实在是太快了,所以我们把运动速度也做了调整
function render() {
renderer.render(scene,camera);
orbit.update();
requestAnimationFrame(render);
//旧代码 uniforms.iTime.value += 0.001;
uniforms.iTime.value += 0.001;
}
最终效果
完整效果由于gif图近10M,csdn承受不了,所以就不发了
案例完整源码
<!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;
uniform sampler2D uDiffuse1;
uniform sampler2D uDiffuse2;
uniform sampler2D uChangeTexture;
uniform float iTime;
void main(){
vec4 col1 = texture2D(uDiffuse1,vec2(vUv.x + iTime,vUv.y));
vec4 col2 = texture2D(uDiffuse2,vec2(vUv.x + iTime,vUv.y));
vec4 col3 = texture2D(uChangeTexture,vec2(abs(vUv.x - 0.5) * 2.0,vUv.y));
gl_FragColor = mix(col2,col1,col3.r);
}
</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(15,15,15);
scene.add(camera);
orbit = new OrbitControls(camera,renderer.domElement);
orbit.enableDamping = true;
scene.add(new THREE.GridHelper(10,10));
}
let uniforms = {
uDiffuse1:{value:null},
uDiffuse2:{value:null},
uChangeTexture:{value:null},
iTime:{value:0.01}
}
function addMesh() {
let textureLoader = new THREE.TextureLoader();
uniforms.uDiffuse1.value = textureLoader.load('./earth_atmos_2048.jpg');
uniforms.uDiffuse1.value.wrapT = THREE.RepeatWrapping;
uniforms.uDiffuse1.value.wrapS = THREE.RepeatWrapping;
uniforms.uDiffuse2.value = textureLoader.load('./earth_lights_2048.png');
uniforms.uDiffuse2.value.wrapT = THREE.RepeatWrapping;
uniforms.uDiffuse2.value.wrapS = THREE.RepeatWrapping;
uniforms.uChangeTexture.value = textureLoader.load('./transition5.png');
uniforms.uChangeTexture.value.wrapT = THREE.RepeatWrapping;
uniforms.uChangeTexture.value.wrapS = THREE.RepeatWrapping;
let geometry = new THREE.SphereGeometry(5,32,32);
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);
uniforms.iTime.value += 0.001;
}
</script>
</body>
</html>
如有不明白的,可以在下方留言或者加群
如有其他不懂的问题,可以在下方留言,也可以加入qq群咨询,本人的群于2024/7/8日正式创建,群号867120877,欢迎大家来群里交流