shader理解

1、什么是shader

所谓shader中文叫做着色器,它实际上是给用户一种方式来介入GPU渲染流程,定制gpu如何组织数据和绘制数据到屏幕上

2、顶点着色器 Vertex Shader

顶点着色器主要负责处理顶点数据,其实顶点着色器能做的事情并不多,大部分就是在处理顶点的矩阵变换,将顶点的位置通过MVP矩阵乘法最终变换到裁剪空间

输入:顶点着色器的输入数据一般是我们传入的attribute、uniforms变量。

输出:一般顶点着色器的运算结果输出是设置gl_Position,也可以设置一些变量比如gl_PointSize或者 varying变量

3、片元着色器fragment Shader

片元着色器在整个渲染中起到了非常大的作用,一般颜色,贴图采样,光照,阴影等计算都会在片元着色器中计算。

输入:片元着色器的输入数据一般是我们从顶点着色器传入的varying或者全局的uniforms变量。

输出:一般片元着色器的运算结果输出是设置gl_FragColor

4、shader是如何运行的

想要了解shader是如何运行的,我们就要先知道整个webgl的运行机制。webgl的一次绘制,需要经过大致的以下几个阶段。

  • 创建webgl的应用程序Program,从文本编译并使用shader
  • 将三维几何数据通过attribute传送给GPU
  • GPU执行顶点着色器,处理顶点数据
  • GPU执行片元着色器,处理颜色等数据
  • 将执行结果写入缓冲区(用于显示到屏幕或者后处理)
  • 我们可以看到,shader的执行是需要链接、编译后执行的,所以shader在运行时其实本身是不能修改的,但是我们可以修改一些数据参数值。

5、如何在threejs中编写shader

ok了解完了基本的一些基础概念后,我们来从一个例子里面,讲讲如何实际使用shader。由于threejs已经帮我们完成了很多基础的框架操作,我们只需要把精力专注在shader程序本身就好。

首先是shader存放的位置,我们可以将shader写成单独的文件,或者在js代码中使用字符串的形式,或者使用html页面中。我们本次演示的是html直接标签包含的形式

第一步,在html的页面中加入以上两个shader的script标签。

<script id="vertexShader" type="x-shader/x-vertex">

    precision mediump float;
    precision mediump int;

    uniform mat4 modelViewMatrix;
    uniform mat4 projectionMatrix;

    attribute vec3 position;

    varying vec3 vPosition;

    void main() {

        vPosition = position;
        gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );

    }

</script>
<script id="fragmentShader" type="x-shader/x-fragment">

    precision mediump float;
    precision mediump int;

    uniform float ratio;

    varying vec3 vPosition;

    void main() {
        vec3 center = vec3( 0.0,0.0,0.0 );
        float dist=  distance(vPosition,center)/100.0; 
        dist = clamp(dist,0.0,1.0); 
        float color = 1.0-dist ; 
        gl_FragColor =  vec4( color*ratio, color*ratio,0.0,dist );

    }

</script>

shader本身的代码量非常少,只做简单的demo用。后面我们会讲解里面具体的内容。我们先继续整个流程

第二步,在我们的js代码中使用threejs的RawShaderMaterial来创建一个shader材质。并将html标签中的内容获取赋值给vertexShader,fragmentShader。同时,我们创建了一个uniform 名叫ratio。

const material = new THREE.RawShaderMaterial({ 
    uniforms: {
        ratio: {
            value: 0.0
        }
    },
    vertexShader: document.getElementById('vertexShader').textContent,
    fragmentShader: document.getElementById('fragmentShader').textContent, 
});

最后一步,我们把这个材质放在一个平面上,并在主循环中更新uniform的值

const plane = new THREE.Mesh(geometry, material);
plane.rotateX(-Math.PI / 2); 
scene.add(plane);

let next = 0;
const animate = function () {
    requestAnimationFrame(animate);
    next = next + 0.01;
    if (next > 1)
        next = 0;
    plane.material.uniforms.ratio.value = next;
    renderer.render(scene, camera);
};

6、精度声明

在webgl的shader中,我们可以在第一行使用precision关键字进行精度设置。声明变量精度高低的三个关键子lowp、mediump和highp。注意不同的shader里面有默认值,如果不指定或者指定错误,会导致编译报错

顶点着色器默认精度

precision highp float;
precision highp int;
precision lowp sampler2D;
precision lowp samplerCube;

片元着色器默认精度

precision mediump int;
precision lowp sampler2D;
precision lowp samplerCube;

7、变量声明

shader中的变量,一般分为三大类,分别是attribute、uniform、varying,他们具体不同的使用场景。

attribute 只能在顶点着色器中出现,且赋值的操作一般是由webgl组织数据的时候就已经绑定好了。attribute是用来向顶点着色器传输几何顶点数据的一般来说。

uniform 可以在顶点或者片元着色器中使用。但是uniform的值是只读的,不可以修改它的值,一般用来传递一些全局参数,比如mvp的矩阵等。

varying 的作用是将顶点着色器中的数据传递给片元着色器。这里的数据一般是一些顶点相关的属性,比如每个顶点的颜色。注意varying在传值的时候,会被gpu插值,所以到片元着色器的时候,值与原先的值不一定完全一致。

##glsl预定义的变量
预定义变量可以直接使用

  • gl_Position

    用于vertex shader, 写顶点位置;被图元收集、裁剪等固定操作功能所使用;其内部声明是:highp vec4 gl_Position

  • gl_FragColor

  • gl_FragData

    用于Fragment shader,是个数组,写gl_FragData[n] 为data n, gl_FragColor和gl_FragData是互斥的,不会同时写入

  • gl_FragCoord

    用于Fragment shader,只读, Fragment相对于窗口的坐标位置 x,y,z,1/w; 这个是固定管线图元差值后产生的;z 是深度值; mediump vec4 gl_FragCoord

  • gl_PointSize

    用于vertex shader, 写光栅化后的点大小,像素个数

  • gl_FrontFacing

    用于判断 fragment是否属于 front-facing primitive;只读

  • gl_PointCoord

    仅用于 point primitive; mediump vec2 gl_PointCoord

  • normalMatrix 法线矩阵

  • discard
    片段着色器中有一种特殊的控制流成为discard。使用discard会退出片段着色器,不执行后面的片段着色操作。片段也不会写入帧缓冲区。
    if (color.a < 0.9)
    discard;

8、程序内容

shader中有一个主函数,类似C语言。一般形式是

void main() {}

程序会从主入口开始执行,直到里面的所有代码全部执行完毕。

我们可以看到,示例中的顶点着色器主函数其实只有两句话,第一句是vPosition = position;表示将attribute的值赋值给varying用来传递给片元着色器。另外一句是gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );这句的作用是,通过矩阵运算,计算当前顶点在裁剪空间坐标点。

片元着色器中,其实也没有几句代码,我们大致讲解一下。我们首先定义了一个圆心vec3 center = vec3( 0.0,0.0,0.0 ); ;然后通过内置的distance函数计算当前顶点位置到圆心的距离float dist= distance(vPosition,center)/100.0;,并转化为(0-1)的区间dist = clamp(dist,0.0,1.0);;我们使用float color = 1.0-dist ;创建一个颜色值,约靠近圆心颜色越深。最后使用gl_FragColor = vec4( colorratio, colorratio,0.0,dist );给当前顶点着色输出。

通过代码分析,我们可以看到,其实shader的作用对象,都是非常微观的顶点,这是由于gpu并行运算的原因。所以,我们并不能知道顶点之间的关系。记住这点,我们在写shader的时候要时刻明白自己操作的对象,否则很容易陷入逻辑误区。

对于矩阵理解很费劲的话,强烈推荐看一看《3d数学基础》,里面介绍的很详细能学到很多知识,百度网盘链接,提取码:h1mi

9、三维渲染机制

设想在世界坐标系中,有一个任意方向任意位置的物体,我们要把它渲染到任意位置任意方向的摄像机中,为了做到这一点,必须将物体的所有顶点从物体坐标系中转换到世界坐标系中,接着再从世界坐标系中转换到摄像机坐标系中
P世界=P物体M物体→世界
P相机=P世界M世界→相机

10、举例

1、渲染一个大气层

var vertexShader	= [
			// 注意:varying定义的变量可以直接传递给片源着色器
			// 顶点着色器的变量传递给片元着色器
			
			//               世界坐标系模型顶点位置
			'varying vec3	vVertexWorldPosition;',
			
			//               单位法线向量
			'varying vec3	vVertexNormal;',
			//               颜色
			'varying vec4	vFragColor;',

			'void main(){',		  
								//normalize 归一化函数 转为单位矢量
								//将法线向量转换到视图坐标系中
			'	vVertexNormal	= normalize(normalMatrix * normal);',
										//将顶点转换到世界坐标系中
										// position 世界坐标系下的位置
			'	vVertexWorldPosition	= (modelMatrix * vec4(position, 1.0)).xyz;',

			'	// set gl_Position',
							// 通过矩阵运算,计算当前顶点在裁剪空间坐标点
			'	gl_Position	= projectionMatrix * modelViewMatrix * vec4(position, 1.0);',
			'}'

		].join('\n');

		var fragmentShader2	= [
			'uniform vec3	glowColor;',//传递全局 只读    发光颜色
			'uniform float	coeficient;',
			'uniform float	power;',

			'varying vec3	vVertexNormal;',
			'varying vec3	vVertexWorldPosition;',

			'varying vec4	vFragColor;',

			'void main(){',
										//世界坐标系中顶点位置到相机位置的向量
			'	vec3 worldVertexToCamera= cameraPosition - vVertexWorldPosition;',	
										//视图坐标系中从相机位置到顶点位置的向量
			'	vec3 viewCameraToVertex	= (viewMatrix * vec4(worldVertexToCamera, 0.0)).xyz;',
										//规一化 viewCameraToVertex视图坐标系中点到摄像机的距离向量
			'	viewCameraToVertex	= normalize(viewCameraToVertex);',
				//                                     视图坐标系下法线向量        视图坐标系下相机到顶点的单位向量
			'	float intensity		= coeficient + dot(vVertexNormal, viewCameraToVertex);',
			//'	if(intensity > 0.55){ intensity = 0.0;}',
			'	gl_FragColor		= vec4(glowColor, intensity);',
			'}'
		].join('\n');
		//本质透明度递减
		var sphere =  new THREE.SphereBufferGeometry( 12, 32, 32 );
		material_glow	= new THREE.ShaderMaterial({
			uniforms: {
				coeficient	: {
					type	: "f",
					value	: 0.0
				},
				power		: {
					type	: "f",
					value	: 2
				},
				glowColor	: {
					type	: "c",
					value	: new THREE.Color('blue')
				}
			},
			vertexShader	: vertexShader,
			fragmentShader	: fragmentShader2,
			blending	: THREE.NormalBlending,
			transparent	: true

		});
		mesh = new THREE.Mesh(sphere, material_glow);

代码解释

总体过程: 我们在世界坐标系中创建了一个球、添加了相机,然后将其转换为统一的相机坐标系(视图坐标系),判断法向量与相机至顶点向量间的夹角大小(即向量点积反应向量的方向的相似程度),来确定片源着色器渲染像素的透明度

先理解几个概念

  • 相机坐标系
    渲染一般是在相机坐标下判断位置关系,模拟眼睛看的世界,最终转换为二位平面渲染到屏幕上,示意图如下
    在这里插入图片描述
  • 法向量
    如下图绿色即为法向量,三维模型是用一个一个的点构成的,在前面的各种空间变换中,对顶点的位置信息进行了各种计算,但是要想构成一个模型光知道顶点的位置信息是不够的,有一个问题就是,如果我们用几个点构成了一个平面之后,我们该怎么确定哪一面是平面的正面?
    在这里插入图片描述

法线就是为了确定模型的正面是朝向哪里而存在的。法线(normal)是定义一个点的朝向的信息,它是一个矢量信息,所以也被称为法矢量(normal vector)。当我们变换一个模型的时候,也必须要变换模型的顶点法线,用来在后续的渲染步骤中计算光照,法线可以说是shader中最重要的信息之一,法线出错会导致各种奇怪的显示问题。而如果利用好法线,则可以在shader中实现各种炫酷的效果。

法线有一个特殊的空间为切线空间,切线矢量(tangent vector)也是顶点带有的一个信息。构成一个三维坐标空间的要素是原点和三个互相垂直的矢量。每个顶点有其自身的切线空间,也就是说每个顶点都是自身切线空间的原点,而法线这个矢量就是它的切线空间的Z轴。它的X轴和Y轴是怎么定义的?

在理解切线空间之前必须先理解一下纹理空间,如果想要把一张图片贴到3D模型上面,就必须要把模型的每个位置对应到图片上,等于是把一个物体的皮扒下来展平在图片上。这个图片所在的空间就是纹理空间,纹理空间是一个二维空间,每一个三维模型的顶点都在纹理空间有一个确定的坐标。

在这里插入图片描述
这也是为什么物体顶点向量转为相机坐标系下是这样(viewMatrix * vec4(worldVertexToCamera, 0.0)).xyz,然而法向量转为相机坐标系下是这样normalMatrix * normal

视图坐标系下法线向量 与相机到顶点的单位向量夹角如下

在这里插入图片描述

最终渲染效果

在这里插入图片描述

GLSL语言基础

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值