length() 内置函数可以获取向量的长度,
这里用 vUv 计算每个像素离原点(0.0, 0.0)位置的距离 dist,将其设置到颜色上,
会得到圆心在左下角的1/4渐变圆形效果,左上角(0.0, 1.0)和右下角(1.0, 0.0)离原点距离都是1,
对应颜色正好是白色,当dist>1后,颜色仍为白色。
颜色突变 GLSL函数 step 、fract
除了渐变,我们可以结合 GLSL 的内置函数做出颜色突变的效果,借助 step(edge, x) 函数,其会返回0.0或1.0数值,如果 x<edge 返回0.0,如果 x>edge 返回1.0。
step(0.5, vUv.x) 通过 vUv.x 和 0.5(vUv中心点 0.5 0.5) 比较,小于0.5的返回0.0,大于0.5的返回1.0,并将该 color 变成转换成 vec3() 格式,于是就是黑白突变的格式。
float color = step(0.5, vUv.x);
gl_FragColor = vec4(vec3(color), 1.0);
fract() 函数取小数使得数值在 0.0-1.0 里循环重复,
比如1.1、2.1取小数后都变回0.1,再将该数值转换成 vec3 再设置到颜色上,
就会产生重复的黑白渐变效果。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<style>
body {
width: 100%;
height: 100%;
}
* {
margin: 0;
padding: 0;
}
.label {
font-size: 20px;
color: #fff;
font-weight: 700;
}
</style>
</head>
<body>
<div id="container"></div>
<script type="importmap">
{
"imports": {
"three": "../three-155/build/three.module.js",
"three/addons/": "../three-155/examples/jsm/"
}
}
</script>
<script type="module">
import * as THREE from 'three';
import Stats from 'three/addons/libs/stats.module.js';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { GPUStatsPanel } from 'three/addons/utils/GPUStatsPanel.js';
import { CSS2DRenderer, CSS2DObject } from 'three/addons/renderers/CSS2DRenderer.js';
let stats, labelRenderer, gpuPanel, shaderMaterial, boxShaderMaterial;
let camera, scene, renderer, mesh, target, controls, shaderMaterialSphere;
const group = new THREE.Group();
let time = 0;
let time1 = 0;
init();
initHelp();
initLight();
axesHelperWord();
animate();
const box = boxModel();
box.position.set(0, 120, 0);
scene.add(box);
let fragmentShader11 = [
"varying vec2 vUv;",
"uniform float uTime;",
"void main(){",
" gl_FragColor = vec4(vUv, 1.0, 1.0);",
"}"
].join('\n');
const sphere11 = sphereModel1(fragmentShader11);
sphere11.position.set(250, 120, -250);
scene.add(sphere11);
let fragmentShader12 = [
"varying vec2 vUv;",
"uniform float uTime;",
"void main(){",
" float color = step(0.5, vUv.y);",
" gl_FragColor = vec4(vec3(color), 1.0);",
"}"
].join('\n');
const sphere12 = sphereModel1(fragmentShader12);
sphere12.position.set(250, 120, -100);
scene.add(sphere12);
let fragmentShader13 = [
"varying vec2 vUv;",
"uniform float uTime;",
"void main(){",
" float color = step(uTime, fract(vUv.y * 5.0));",
" gl_FragColor = vec4(vec3(color), 1.0);",
"}"
].join('\n');
const sphere13 = sphereModel1(fragmentShader13);
sphere13.position.set(250, 120, 50);
scene.add(sphere13);
function sphereModel1(fragmentShader) {
let vertexShader = [
"varying vec2 vUv;",
"void main(){",
" gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);",
" vUv = uv;",
"}"
].join('\n');
/**
* 颜色渐变
* 就是照理 position 是每个顶点的坐标,uv 是每个顶点的纹理坐标
* shader 里的代码是对每个顶点或片元单独执行的,这里的 vUv 就只是每个片元其各自的数值
* (每个片元甚至连周围片元的数值是多少都不知道),
* 比如左下角 vUv 为 (0.0, 0.0) 所以对应的 vUv.x=vUv.y=0.0,颜色为 vec4(0.0, 0.0, 0.0, 1.0)
* 即黑色,同理把左上角 (0.0, 1.0)、右下角 (1.0, 0.0)、右上角 (1.0, 1.0)、最中间 (0.5, 0.5)
* 等每个位置的数值分别带入上面的代码,就能得到上图的效果。
*/
// gl_FragColor.a = 0.9;
// gl_FragColor.rgb = vec3(1, 1, 1)
// 如果用 vUv 里的 x 或 y 分量分别设置到 rgba 颜色里的 red 通道并赋值给
// gl_FragColor,会有左右或者上下的黑色到红色的渐变效果。
// gl_FragColor = vec4(vUv.x, 0.0, 0.0, 1.0);
// gl_FragColor = vec4(vUv.y, 0.0, 0.0, 1.0);
// gl_FragColor = vec4(vec3(vUv.x), 1.0);
// gl_FragColor = vec4(vec3(vUv.y), 1.0);
// 如果将 vUv 设置到 red 和 green 通道、blue 通道设为0.0,
// 就是这个非常常见的 uv 青色红色颜色效果,如果大家用过其他一些3D软件,应该对这个图并不陌生。
// gl_FragColor = vec4(vUv, 0.0, 1.0);
/**
* 颜色突变 GLSL函数 step 、fract
除了渐变,我们可以结合 GLSL 的内置函数做出颜色突变的效果,借助 step(edge, x) 函数,
其会返回0.0或1.0数值,如果 x<edge 返回0.0,如果 x>edge 返回1.0。step(0.5, vUv.x)
通过 vUv.x 和 0.5(vUv中心点 0.5 0.5) 比较,小于0.5的返回0.0,大于0.5的返回1.0,并将该 color 变成转换成 vec3() 格式,
于是就是黑白突变的格式。
float color = step(0.5, vUv.x);
gl_FragColor = vec4(vec3(color), 1.0);
*/
/**
* fract() 函数取小数使得数值在 0.0-1.0 里循环重复,
* 比如1.1、2.1取小数后都变回0.1,再将该数值转换成 vec3 再设置到颜色上,
* 就会产生重复的黑白渐变效果。
*/
const planeGeo = new THREE.SphereGeometry(45, 30, 30);
shaderMaterialSphere = new THREE.ShaderMaterial({
vertexShader: vertexShader,
fragmentShader: fragmentShader,
side: THREE.DoubleSide,
uniforms: {
uTime: { value: 0.5 },
},
transparent: false,
depthWrite: true
});
return new THREE.Mesh(planeGeo, shaderMaterialSphere);
}
function boxModel() {
let vertexShader = [
"varying vec2 vUv;",
"void main(){",
" gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);",
" vUv = uv;",
"}"
].join('\n');
// 当我们有了每个 uv 离中心的距离后,可以对它运用翻倍再取小数的操作进行重复,
// 这样就能做出由里向外一圈圈黑白交错的径向条纹效果。
// 有个小细节需要注意,上面我们翻了5倍,但看效果里一黑一白为1组,其实只有3组多点,
// 究其原因是 length(vUv - vec2(0.5)) 一开始的范围并不是0到1,最大值是四个角离中心的距离,
// 也就是 (1.0, 1.0) 离 (0.5, 0.5) 的距离,
// 即 √2/2=0.707,因而我们可以先对其除以 0.707 再去翻倍取小数进行重复,
// 这样就能如愿想有几组就几组、想重复几次就重复几次。
// 然后将 uTime 加到变换到0-1范围后的数值上,使得径向条纹动起来
// 反向运动可以减去 uTime,运动速率可以通过 uTime 的倍数来控制。
let fragmentShader = `
varying vec2 vUv;
uniform float uTime;
void main() {
// 先居中,再绘制圆形
float dist = fract((length(vUv - vec2(0.5)) / 0.707 - uTime * 0.5) * 5.0);
float radius = 0.5;
vec3 color = vec3(step(radius, dist));
gl_FragColor = vec4(color, 1.0);
}
`;
const planeGeo = new THREE.BoxGeometry(70, 70, 70);
boxShaderMaterial = new THREE.ShaderMaterial({
vertexShader: vertexShader,
fragmentShader: fragmentShader,
side: THREE.DoubleSide,
uniforms: {
uTime: { value: 0 },
},
transparent: false,
depthWrite: true
});
return new THREE.Mesh(planeGeo, boxShaderMaterial);
}
/**
* 通过 length() 内置函数可以获取向量的长度,
* 这里用 vUv 计算每个像素离原点(0.0, 0.0)位置的距离 dist,将其设置到颜色上,
* 会得到圆心在左下角的1/4渐变圆形效果,左上角(0.0, 1.0)和右下角(1.0, 0.0)离原点距离都是1,
* 对应颜色正好是白色,当dist>1后,颜色仍为白色。
*/
let fragmentShader1 = `
varying vec2 vUv;
uniform float uTime;
void main() {
float dist = length(vUv);
vec3 color = vec3(dist);
gl_FragColor = vec4(color, 1.0);
}
`;
const sphere1 = sphereModel(fragmentShader1);
sphere1.position.set(-250, 120, -250);
scene.add(sphere1);
// 绘制渐变圆形
let fragmentShader2 = `
varying vec2 vUv;
uniform float uTime;
void main() {
float dist = length(vUv);
vec3 color = vec3(step(0.5, dist));
gl_FragColor = vec4(color, 1.0); // 1.0 是透明度
}
`;
const sphere2 = sphereModel(fragmentShader2);
sphere2.position.set(-250, 120, -100);
scene.add(sphere2);
let fragmentShader3 = `
varying vec2 vUv;
uniform float uTime;
void main() {
// float dist = length(vUv - vec2(0.5));
float dist = distance(vUv, vec2(0.5));
float radius = 0.5; // 0.25
vec3 color = vec3(step(radius, dist));
gl_FragColor = vec4(color, 1.0);
}
`;
const sphere3 = sphereModel(fragmentShader3);
sphere3.position.set(-250, 120, 50);
scene.add(sphere3);
let fragmentShader4 = `
varying vec2 vUv;
uniform float uTime;
void main() {
// 先重复 uv,再居中,再绘制圆形
float dist = length(fract(vUv * 5.0) - vec2(0.5));
float radius = 0.5; // 0.25
vec3 color = vec3(step(radius, dist));
gl_FragColor = vec4(color, 1.0);
}
`;
const sphere4 = sphereModel(fragmentShader4);
sphere4.position.set(-250, 120, 200);
scene.add(sphere4);
let fragmentShader5 = `
varying vec2 vUv;
uniform float uTime;
void main() {
// 先居中,再绘制圆形
float dist = length(fract(vUv * 5.0) - vec2(0.5));
// float dist = distance(vUv, vec2(0.5));
float radius = 0.5 * (sin(uTime + vUv.x) * 0.5 + 0.5); // 0.25
vec3 color = vec3(step(radius, dist));
gl_FragColor = vec4(color, 1.0);
}
`;
const sphere5 = sphereModel(fragmentShader5);
sphere5.position.set(-250, 120, 350);
scene.add(sphere5);
function sphereModel(fragmentShader) {
let vertexShader = [
"varying vec2 vUv;",
"void main(){",
" gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);",
" vUv = uv;",
"}"
].join('\n');
/**
// 绘制渐变圆形
float dist = length(vUv);
vec3 color = vec3(dist);
gl_FragColor = vec4(color, 1.0);
// 绘制圆形
float dist = length(vUv);
vec3 color = vec3(step(0.5, dist));
gl_FragColor = vec4(color, 1.0);
// 先居中,再绘制圆形
float dist = length(vUv - vec2(0.5));
// float dist = distance(vUv, vec2(0.5));
float radius = 0.5; // 0.25
vec3 color = vec3(step(radius, dist));
gl_FragColor = vec4(color, 1.0);
// 先重复 uv,再居中,再绘制圆形
float dist = length(fract(vUv * 5.0) - vec2(0.5));
但此时每个圆圈都是通过 sin(uTime) 控制动画,半径变化同步进行,很统一也很单调,
这里可以通过 sin(uTime + vUv.x) 将不同水平值作为偏差值加进去,
于是会有水平波浪起伏的效果
如果再把 vUv.y 也一起加上,变化效果更有趣丰富。
float radius = 0.5 * (sin(uTime + vUv.x + vUv.y) * 0.5 + 0.5);
*/
const planeGeo = new THREE.BoxGeometry(70, 70, 70);
shaderMaterial = new THREE.ShaderMaterial({
vertexShader: vertexShader,
fragmentShader: fragmentShader,
side: THREE.DoubleSide,
uniforms: {
uTime: { value: 0 },
},
transparent: false,
depthWrite: true
});
return new THREE.Mesh(planeGeo, shaderMaterial);
}
function init() {
camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 10, 2000);
camera.position.set(0, 500, 500);
camera.up.set(0, 1, 0);
camera.lookAt(0, 0, 0);
scene = new THREE.Scene();
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
labelRenderer = new CSS2DRenderer();
labelRenderer.setSize(window.innerWidth, window.innerHeight);
labelRenderer.domElement.style.position = 'absolute';
labelRenderer.domElement.style.top = '0px';
labelRenderer.domElement.style.pointerEvents = 'none';
document.getElementById('container').appendChild(labelRenderer.domElement);
window.addEventListener('resize', onWindowResize);
controls = new OrbitControls(camera, renderer.domElement);
controls.minDistance = 10;
controls.maxDistance = 1000;
// 设置为true可启用阻尼(惯性),可用于为控件提供重量感。默认值为false。
// 请注意,如果启用了此选项,则必须在动画循环中调用.update()。
controls.enableDamping = false;
controls.screenSpacePanning = false; // 定义平移时如何平移相机的位置 控制不上下移动
stats = new Stats();
document.body.appendChild(stats.dom);
gpuPanel = new GPUStatsPanel(renderer.getContext());
stats.addPanel(gpuPanel);
stats.showPanel(0);
scene.add(group);
}
function initLight() {
const light = new THREE.DirectionalLight(new THREE.Color('rgb(253,253,253)'));
light.position.set(100, 100, -10);
light.intensity = 3; // 光线强度
light.castShadow = true; // 是否有阴影
light.shadow.mapSize.width = 2048; // 阴影像素
light.shadow.mapSize.height = 2048;
// 阴影范围
const d = 80;
light.shadow.camera.left = -d;
light.shadow.camera.right = d;
light.shadow.camera.top = d;
light.shadow.camera.bottom = -d;
light.shadow.bias = -0.0005; // 解决条纹阴影的出现
// 最大可视距和最小可视距
light.shadow.camera.near = 0.01;
light.shadow.camera.far = 2000;
const AmbientLight = new THREE.AmbientLight(new THREE.Color('rgb(255,255,255)'));
scene.add(light);
scene.add(AmbientLight);
}
function initHelp() {
const size = 1000;
const divisions = 20;
const gridHelper = new THREE.GridHelper(size, divisions);
scene.add(gridHelper);
// The X axis is red. The Y axis is green. The Z axis is blue.
const axesHelper = new THREE.AxesHelper(500);
scene.add(axesHelper);
}
function axesHelperWord() {
let xP = addWord('X轴');
let yP = addWord('Y轴');
let zP = addWord('Z轴');
xP.position.set(400, 0, 0);
yP.position.set(0, 400, 0);
zP.position.set(0, 0, 400);
}
function addWord(word) {
let name = `<span>${word}</span>`;
let moonDiv = document.createElement('div');
moonDiv.className = 'label';
// moonDiv.textContent = 'Moon';
// moonDiv.style.marginTop = '-1em';
moonDiv.innerHTML = name;
const label = new CSS2DObject(moonDiv);
group.add(label);
return label;
}
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
function animate() {
time += 0.05;
if (shaderMaterial) {
shaderMaterial.uniforms.uTime.value = time;
}
time1 += 0.01;
if (boxShaderMaterial) {
boxShaderMaterial.uniforms.uTime.value = time1;
}
if (shaderMaterialSphere) {
shaderMaterialSphere.uniforms.uTime.value += 0.005;
if (shaderMaterialSphere.uniforms.uTime.value >= 0.9) {
shaderMaterialSphere.uniforms.uTime.value = -0.1;
}
}
requestAnimationFrame(animate);
stats.update();
controls.update();
labelRenderer.render(scene, camera);
renderer.render(scene, camera);
}
</script>
</body>
</html>