用three.js写一个3D地球
着色器的入门介绍
Webgl绘制图形是基于着色器(shader)的绘图机制,着色器提供了灵活且强大的绘制二维或三维图形的方法,所有Webgl程序必须使用它。
着色器语言类似于c语言,当我们写webgl程序时,着色器语言以字符串的形式嵌入在javascript语言中。
比如,要在屏幕上绘制一个点,代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
body {
margin: 0
}
</style>
</head>
<body>
<canvas id="webgl"></canvas>
</body>
<script>
//将canvas的大小设置为屏幕大小
var canvas = document.getElementById('webgl')
canvas.height = window.innerHeight
canvas.width = window.innerWidth
//获取webgl绘图上下文
var gl = canvas.getContext('webgl')
//将背景色设置为黑色
gl.clearColor(0.0, 0.0, 0.0, 1.0)
gl.clear(gl.COLOR_BUFFER_BIT)
//顶点着色器代码(字符串形式)
var VSHADER_SOURCE =
`void main () {
gl_Position = vec4(0.5, 0.5, 0.0, 1.0); //点的位置:x: 0.5, y: 0.5, z: 0。齐次坐标
gl_PointSize = 10.0; //点的尺寸,非必须,默认是0
}`
//片元着色器代码(字符串形式)
var FSHADER_SOURCE =
`void main () {
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); //点的颜色:四个量分别代表 rgba
}`
//初始化着色器
initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)
//绘制一个点,第一个参数为gl.POINTS
gl.drawArrays(gl.POINTS, 0, 1)
function initShaders(gl, vshader, fshader) {
var program = createProgram(gl, vshader, fshader);
if (!program) {
console.log('Failed to create program');
return false;
}
gl.useProgram(program);
gl.program = program;
return true;
}
function createProgram(gl, vshader, fshader) {
var vertexShader = loadShader(gl, gl.VERTEX_SHADER, vshader);
var fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fshader);
if (!vertexShader || !fragmentShader) {
return null;
}
var program = gl.createProgram();
if (!program) {
return null;
}
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
var linked = gl.getProgramParameter(program, gl.LINK_STATUS);
if (!linked) {
var error = gl.getProgramInfoLog(program);
console.log('Failed to link program: ' + error);
gl.deleteProgram(program);
gl.deleteShader(fragmentShader);
gl.deleteShader(vertexShader);
return null;
}
return program;
}
function loadShader(gl, type, source) {
// 创建着色器对象
var shader = gl.createShader(type);
if (shader == null) {
console.log('unable to create shader');
return null;
}
gl.shaderSource(shader, source);
gl.compileShader(shader);
var compiled = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
if (!compiled) {
var error = gl.getShaderInfoLog(shader);
gl.deleteShader(shader);
return null;
}
return shader;
}
</script>
</html>
上面代码在屏幕右上区域绘制了一个点。
绘制这个点需要三个必要的信息:位置、尺寸和颜色。
- 顶点着色器指定点的位置和尺寸。(下面的代码中,
gl_Position
、gl_PointSize
和gl_FragColor
都是着色器的内置全局变量。)
var VSHADER_SOURCE =
`void main () {
gl_Position = vec4(0.5, 0.5, 0.0, 1.0); //指定点的位置
gl_PointSize = 10.0; //指定点的尺寸
}`
- 片元着色器指定点的颜色。
var FSHADER_SOURCE =
`void main () {
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); //指定点的颜色
}`
attribute变量 和 uniform变量
上面的例子中,我们直接在着色器中指定了点的位置、尺寸和颜色。而实际操作中,这些信息基本都是由js传递给着色器。
用于 js代码 和 着色器代码 通信的变量是attribute变量
和uniform变量
。
使用哪一种变量取决于需要传递的数据本身,attribute变量
用于传递与顶点相关的数据,uniform变量
用于传递与顶点无关的数据。
下面的例子中,要绘制的点的坐标将由js传入。
//顶点着色器
var VSHADER_SOURCE =
`attribute vec4 a_Position; //声明一个attribute变量a_Position,用于接受js传递的顶点位置
void main () {
gl_Position = a_Position; //将a_Position赋值给gl_Position
gl_PointSize = 10.0;
}`
//片元着色器
var FSHADER_SOURCE =
`void main () {
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}`
initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)
//js代码中,获取a_Position的存储位置,并向其传递数据
var a_Position = gl.getAttribLocation(gl.program, 'a_Position')
gl.vertexAttrib3f(a_Position, 0.5, 0.5, 0.0)
gl.drawArrays(gl.POINTS, 0, 1)
varying变量
我们从js传给着色器的通常是顶点相关的数据,比如我们要绘制一个三角形,三角形的顶点位置和顶点颜色由js传入。三个顶点的位置可以确定三角形的位置,那么整个三角形的颜色由什么确定呢?
这就需要varying变量出场了。
webgl中的颜色计算:
在顶点着色器
中,接收js传入的每个顶点的位置和颜色数据。webgl系统会根据顶点的数据,插值计算
出,顶点之间区域中,每个片元(可以理解为组成图像的最小渲染点)的颜色值。插值计算由webgl系统自动完成。
计算出的每个片元的颜色值,再传递给 片元着色器
。片元着色器
根据每个片元的颜色值渲染出图像。
从顶点着色器
到 片元着色器
,传递工作由varying变量
完成。
代码如下。
- 顶点着色器代码
var VSHADER_SOURCE =
`attribute vec4 a_Position; //顶点位置
attribute vec4 a_Color; //顶点颜色
varying vec4 v_Color; //根据顶点颜色,计算出三角形中每个片元的颜色值,然后将每个片元的颜色值传递给片元着色器。
void main () {
gl_Position = a_Position;
v_Color = a_Color; // a_Color 赋值给 v_Color
}`
- 片元着色器代码
var FSHADER_SOURCE =
`precision mediump float;
varying vec4 v_Color; //每个片元的颜色值
void main () {
gl_FragColor = v_Color;
}`
- js代码
var verticesColors = new Float32Array([ //顶点位置和颜色
0.0, 0.5, 1.0, 0.0, 0.0, // 第一个点,前两个是坐标(x,y; z默认是0),后三个是颜色
-0.5, -0.5, 0.0, 1.0, 0.0, // 第二个点
0.5, -0.5, 0.0, 0.0, 1.0 // 第三个点
])
//以下是通过缓冲区向顶点着色器传递顶点位置和颜色
var vertexColorBuffer = gl.createBuffer()
gl.bindBuffer(gl.ARRAY_BUFFER, vertexColorBuffer)
gl.bufferData(gl.ARRAY_BUFFER, verticesColors, gl.STATIC_DRAW)
var FSIZE = verticesColors.BYTES_PER_ELEMENT
var a_Position = gl.getAttribLocation(gl.program, 'a_Position')
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, FSIZE * 5, 0)
gl.enableVertexAttribArray(a_Position)
var a_Color = gl.getAttribLocation(gl.program, 'a_Color')
gl.vertexAttribPointer(a_Color, 3, gl.FLOAT, false, FSIZE * 5, FSIZE * 2)
gl.enableVertexAttribArray(a_Color)
//绘制一个三角形,第一个参数为gl.TRIANGLES
gl.drawArrays(gl.TRIANGLES, 0, 3)
下面最终绘制出来的效果:
纹理映射的简单理解
在上面的例子中,我们是为每个顶点指定颜色值。
延伸一下,纹理映射是为每个顶点指定纹理坐标,然后webgl系统会根据顶点纹理坐标,插值计算出每个片元的纹理坐标。
然后在片元着色器中,会根据传入的纹理图像,以及每个片元的纹理坐标,取出纹理图像中对应纹理坐标上的颜色值(纹素),作为该片元的颜色值,并进行渲染。
纹理坐标的特点:
- 纹理图像左下角为原点(0, 0)。
- 向右为横轴正方向,横轴最大值为 1(图像右边缘)。
- 向上为纵轴正方向,纵轴最大值为 1(图像上边缘)。
不管纹理图像的尺寸是多少,纹理坐标的范围都是: x轴:0-1,y轴:0-1
画一个3D地球
使用webgl进行绘制,步骤和API都比较繁琐,所幸我们可以借助three.js
。
three.js
中的ShaderMaterial
可以让我们自己定制着色器,直接操作像素。我们只需要理解着色器的基本原理。
开始画地球吧。
基础球体
基础球体的绘制比较简单,用three.js
提供的材质就行。关于材质的基础,在 用three.js写一个反光球 有比较详细的介绍。
var loader = new THREE.TextureLoader()
var group = new THREE.Group()
//创建本体
var geometry = new THREE.SphereGeometry(20,30,30) //创建球形几何体
var earthMaterial = new THREE.MeshPhongMaterial({ //创建材质
map: loader.load( './images/earth.png' ), //基础纹理
specularMap: loader.load('./images/specular.png'), //高光纹理,指定物体表面中哪部分比较闪亮,哪部分相对暗淡
normalMap: loader.load('./images/normal.png'), //法向纹理,创建更加细致的凹凸和褶皱
normalScale: new THREE.Vector2(3, 3)
})
var sphere = new THREE.Mesh(geometry, earthMaterial) //创建基础球体
group.add(sphere)
流动大气
使用ShaderMaterial
自定义着色器。大气的流动,是通过每次在requestAnimationFrame
渲染循环中改变纹理坐标实现。为了使流动更加自然,加入噪声。
//顶点着色器
var VSHADER_SOURCE = `
varying vec2 vUv;
void main () {
vUv = uv; //顶点纹理坐标
gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4( position, 1.0 );
}
`
//片元着色器
var FSHADER_SOURCE = `
uniform float time; //时间变量
uniform sampler2D fTexture; //大气纹理图像
uniform sampler2D nTexture; //噪声纹理图像
varying vec2 vUv; //片元纹理坐标
void main () {
vec2 newUv= vUv + vec2( 0, 0.02 ) * time; //向量加法,根据时间变量计算新的纹理坐标
//利用噪声随机使纹理坐标随机化
vec4 noiseRGBA = texture2D( nTexture, newUv );
newUv.x += noiseRGBA.r * 0.2;
newUv.y += noiseRGBA.g * 0.2;
gl_FragColor = texture2D( fTexture, newUv ); //提取大气纹理图像的颜色值(纹素)
}
`
var flowTexture = loader.load('./images/flow.png')
flowTexture.wrapS = THREE.RepeatWrapping
flowTexture.wrapT = THREE.RepeatWrapping
var noiseTexture = loader.load('./images/noise.png')
noiseTexture.wrapS = THREE.RepeatWrapping
noiseTexture.wrapT = THREE.RepeatWrapping
//着色器材质
var flowMaterial = new THREE.ShaderMaterial({
uniforms: {
fTexture: {
value: flowTexture,
},
nTexture: {
value: noiseTexture,
},
time: {
value: 0.0
},
},
// 顶点着色器
vertexShader: VSHADER_SOURCE,
// 片元着色器
fragmentShader: FSHADER_SOURCE,
transparent: true
})
var fgeometry = new THREE.SphereGeometry(20.001,30,30) //创建比基础球体略大的球状几何体
var fsphere = new THREE.Mesh(fgeometry, flowMaterial) //创建大气球体
group.add(fsphere)
scene.add( group )
创建了group
,基础球体和大气球体,都加入到group
,作为一个整体,设置转动和位置,都直接修改group
的属性。
var clock = new THREE.Clock()
//渲染循环
var animate = function () {
requestAnimationFrame(animate)
var delta = clock.getDelta()
group.rotation.y -= 0.002 //整体转动
flowMaterial.uniforms.time.value += delta //改变uniforms.time的值,用于片元着色器中的纹理坐标计算
renderer.render(scene, camera)
}
animate()
光晕
创建光晕用的是精灵(Sprite),精灵是一个总是面朝着摄像机的平面,这里用它来模拟光晕,不管球体怎么转动,都看上去始终处于光晕中。
var ringMaterial = new THREE.SpriteMaterial( { //创建点精灵材质
map: loader.load('./images/ring.png')
} )
var sprite = new THREE.Sprite( ringMaterial ) //创建精灵,和普通物体的创建不一样
sprite.scale.set(53,53, 1) //设置精灵的尺寸
scene.add( sprite )
最终效果图:
链接:https://juejin.cn/post/6992445067344478239