threejs:用着色器给模型添加光带扫描效果

embedded/2025/3/6 0:31:02/

第一步:给模型添加光带

首先创建一个立方体,不进行任何缩放平移操作,也不要set position。

基础代码如下:

在顶点着色器代码里varying vec3 vPosition;vPosition = position;获得threejs自动计算的顶点坐标插值(也就是这个模型上每个点的xy坐标),然后在片元着色器代码里同样varying vec3 vPosition;来获取xy坐标值。

先设置整体颜色gl_FragColor = vec4(0.0,1.0,1.0,1.0);

然后再通过if条件判断,符合条件的片元设置其他颜色,光带就形成了。

gl_Position = projectionMatrix*viewMatrix*modelMatrix*vec4( position, 1.0 );是固定写法,你可以试下去掉会发生什么。

javascript">import * as THREE from 'three';const geometry = new THREE.BoxGeometry(30,60,30);const vertexShader = `varying vec3 vPosition;//表示顶点插值后位置数据,与片元数量相同,一一对应void main(){vPosition = position;// 顶点位置坐标插值计算// 投影矩阵 * 视图矩阵 * 模型矩阵 * 模型顶点坐标gl_Position = projectionMatrix*viewMatrix*modelMatrix*vec4( position, 1.0 );}
`;const fragmentShader = `varying vec3 vPosition;void main(){// 设置整体颜色gl_FragColor = vec4(0.0,1.0,1.0,1.0);// 当vPosition.y的位置符合if条件时,设置其他颜色,就会形成光带if(vPosition.y > 20.0 && vPosition.y < 22.0 ){gl_FragColor = vec4(1.0,1.0,0.0,1.0);}}
`;// 以下代码是使用着色器材料进行颜色设置
export const material = new THREE.ShaderMaterial({//顶点着色器对象vertexShadervertexShader: vertexShader,// 片元着色器对象fragmentShaderfragmentShader: fragmentShader});export const model = new THREE.Mesh(geometry, material);

在y坐标20的地方,有一条宽度为2的光带,因为限制条件是20<y<22。 

第二步:让光带动起来 

想要让光带动起来,只需要限制条件也动起来,比如之前的20<y<22,要让这两个数字随时间发生变化。这时候需要在shader材质里使用uniforms定义一个对象变量startY,包含value属性。

javascript">export const material = new THREE.ShaderMaterial({uniforms:{startY:{value:-30.0} //立方体位于原点,y的最小值是-30.0,而不是0.0},//顶点着色器对象vertexShadervertexShader: vertexShader,// 片元着色器对象fragmentShaderfragmentShader: fragmentShader});

在片元着色器里接收uniform里的变量,名字必须跟shader材质里定义的相同,注意这里是uniform,shader材质里是uniforms,将vPosition.y的范围限定在startY和startY+2.0之间。

javascript">const fragmentShader = `varying vec3 vPosition;uniform float startY;void main(){// 设置整体颜色,不然模型会设置为默认白色gl_FragColor = vec4(0.0,1.0,1.0,1.0);// 当vPosition.y的位置符合if条件时,设置其他颜色,就会形成光带if(vPosition.y > startY && vPosition.y < startY + 2.0 ){gl_FragColor = vec4(1.0,1.0,0.0,1.0);}}
`;

在渲染循环里让startY不断改变,片元着色器里的startY跟着变化,光带就动起来了。

特别注意,startY的起始值是-30.0,而不是0.0,startY的最大值是30.0,由于vPosition.y是浮点型数据,在对其进行计算的变量也必须是浮点型。

javascript">// 渲染循环
function render() {material.uniforms.startY.value += 0.5;// 当y超过模型高度后,y重置到模型底部if(material.uniforms.startY.value>30.0){material.uniforms.startY.value = -30.0;}renderer.render(scene, camera);requestAnimationFrame(render);
}

这时会发现光带移动到顶部的时候,会出现闪烁,我们把startY的值再缩小一点,避免这个问题。

之前是material.uniforms.startY.value>30.0 的时候,startY重置,改成material.uniforms.startY.value>25.0没这个现象了,具体上限是多少,跟模型高度和光带宽度有关,根据自己的实际项目来设置即可。

第三步:美化光带

光带的上半部分,从下往上,从光带颜色渐变到模型本身的颜色;下半部分,从上往下,从光带颜色渐变到模型本身的颜色。

javascript">const fragmentShader = `varying vec3 vPosition;uniform float startY;const float bandWidth = 20.0;//光带宽度float halfBandWidth = bandWidth*0.5;//光带宽度的一半const vec3 bandColor = vec3(1.0,0.0,0.0);//光带的颜色const vec3 baseColor = vec3(0.0,1.0,1.0);//模型本身的颜色void main(){// 设置整体颜色,不然模型会设置为默认白色gl_FragColor = vec4(baseColor,1.0);// 光带上半部分if(vPosition.y > startY && vPosition.y < startY + halfBandWidth ){float percent = (vPosition.y-startY)/halfBandWidth;//范围0~1gl_FragColor.rgb = mix( bandColor, baseColor, percent);}// 光带下半部分if(vPosition.y <= startY && vPosition.y > startY - halfBandWidth ){float percent = (startY - vPosition.y)/halfBandWidth;//范围0~1gl_FragColor.rgb = mix( bandColor, baseColor, percent);}}
`;

为了更方便看渐变色效果,先把光带移动停下了,光带加宽,光带设置为红色。

注意:gl_FragColor是vec4类型,表示片元颜色的rgba,gl_FragColor.rgb表示当前片元颜色的rgb,不带a(透明度)。

mix着色器语言GLSL ES的内置函数,可以直接使用,比如参数1和2分表示一个颜色值,通过参数3百分比per,就可以控制两个颜色color1、color2的混合比例,参数3范围控制在0~1就行。

mix的参数1和2顺序,不用刻意记住,用代码测试下就行,不对就反过来。

mix的两个颜色参数,是vec3的,只包含rgb信息,所以只需要赋值给gl_FragColor.rgb即可,此时默认的透明度是1.0,如果确实需要设置a,可以写成gl_FragColor = vec4( mix( bandColor, baseColor, percent),0.8); 把vec3变成vec4.

要特别注意,光带颜色和模型颜色不要设置为一样,否则就会跟我一样,看不到光带,还以为是代码逻辑有问题,检查了好几遍才发现。

第四步:增加光带

想要显示多条移动光带,startY不仅要在渲染循环量不断变化,还要在for循环变化,直接写startY += float(i);会报错uniform里的变量不能修改,我们得换个方法,定义另一个uniform变量time,在片元着色器里将time赋值给另一个普通的float变量startY,再在for循环了来改变startY。

javascript">// 以下代码是使用着色器材料进行颜色设置
export const material = new THREE.ShaderMaterial({uniforms:{time:{value:0.0} //立方体位于原点,y的最小值是-30.0,而不是0.0},//顶点着色器对象vertexShadervertexShader: vertexShader,// 片元着色器对象fragmentShaderfragmentShader: fragmentShader});
javascript">const fragmentShader = `varying vec3 vPosition;uniform float time;const float bandWidth = 4.0;//光带宽度const float bandSpacing = 4.0;//光带间隔float halfBandWidth = bandWidth*0.5;//光带宽度的一半const vec3 bandColor = vec3(1.0,1.0,0.0);//光带的颜色const vec3 baseColor = vec3(0.0,1.0,1.0);//模型本身的颜色void main(){// 设置整体颜色,不然模型会设置为默认白色gl_FragColor = vec4(baseColor,1.0);float startY = -30.0+time; //-30.0是模型y坐标的起始值//循环产生多条光带for(int i=0;i<10;i++){startY += float(i)+bandSpacing;// 光带上半部分if(vPosition.y > startY && vPosition.y < startY + halfBandWidth ){float percent = (vPosition.y-startY)/halfBandWidth;//范围0~1gl_FragColor.rgb = mix( bandColor, baseColor, percent);}// 光带下半部分if(vPosition.y <= startY && vPosition.y > startY - halfBandWidth ){float percent = (startY - vPosition.y)/halfBandWidth;//范围0~1gl_FragColor.rgb = mix( bandColor, baseColor, percent);}}}
`;
javascript">function render() {material.uniforms.time.value += 0.5;// 当y超过模型高度后,y重置到模型底部if(material.uniforms.time.value>15.0){material.uniforms.time.value = 0.0;}renderer.render(scene, camera);requestAnimationFrame(render);
}

 

运行后看效果,顶部又开始了闪烁,需要将vPosition.y的值需要限制一下。

在if条件里加上vPosition.y <30.0,注意,光带的上半部分和下半部分都要加这句话。

我的模型里是小于30,具体小于多少,以自己的项目来调整。

旋转后发现底部也有闪烁,继续做限制vPosition.y >-30.0。

模型底部没有光带,需要将startY的下限继续下移。把-30改成-40,总之要比模型本身y的最小值更小。当time=0的时候,startY等于-40,比模型的底部更低,具体值多少,以自己的项目来调整。

float startY = -40.0+time; //模型y坐标的起始值。

除了调整以上数据,还可以调整time重置的条件,比如time大于10和大于20,效果是不同的。 

调整后的效果。

第五步:将光带换成彩色

创建光带颜色数组,在for循环里对数组长度循环取值即可。

换成彩色后,光带直接有一个很大的空隙,这是光带数组中有一个光带颜色跟模型本身的颜色一样,mix后就看不出来光带颜色了,这也是一个需要注意的地方,换个模型颜色后,间隔恢复正常了。

完整代码:

javascript">import * as THREE from 'three';const geometry = new THREE.BoxGeometry(30,60,30);const vertexShader = `varying vec3 vPosition;//表示顶点插值后位置数据,与片元数量相同,一一对应void main(){vPosition = position;// 顶点位置坐标插值计算// 投影矩阵 * 视图矩阵 * 模型矩阵 * 模型顶点坐标gl_Position = projectionMatrix*viewMatrix*modelMatrix*vec4( position, 1.0 );}
`;const fragmentShader = `varying vec3 vPosition;uniform float time;const float bandWidth = 4.0;//光带宽度const float bandSpacing = 4.0;//光带间隔float halfBandWidth = bandWidth*0.5;//光带宽度的一半//光带的颜色const vec3 bandColor[7] = vec3[7](vec3(1.0,0.0,0.0), vec3(1.0,0.5,0.0), vec3(1.0,1.0,0.0),vec3(0.0,1.0,0.0), vec3(0.0,1.0,1.0), vec3(0.0,0.0,1.0),vec3(1.0,0.0,1.0));const vec3 baseColor = vec3(1.0,1.0,1.0);//模型本身的颜色void main(){// 设置整体颜色,不然模型会设置为默认白色gl_FragColor = vec4(baseColor,1.0);float startY = -40.0+time; //模型y坐标的起始值float percent = 0.0;int colorIndex = 0;for(int i=0;i<10;i++){startY += float(i)+bandSpacing;colorIndex = int(mod(float(i),float(bandColor.length())));// 光带上半部分if(vPosition.y > startY && vPosition.y < startY + halfBandWidth && vPosition.y <30.0 && vPosition.y >-30.0){percent = (vPosition.y-startY)/halfBandWidth;//范围0~1gl_FragColor.rgb = mix( bandColor[colorIndex], baseColor, percent);}// 光带下半部分if(vPosition.y <= startY && vPosition.y > startY - halfBandWidth && vPosition.y <30.0 && vPosition.y >-30.0){percent = (startY - vPosition.y)/halfBandWidth;//范围0~1gl_FragColor.rgb = mix( bandColor[colorIndex], baseColor, percent);}}}
`;// 以下代码是使用着色器材料进行颜色设置
export const material = new THREE.ShaderMaterial({uniforms:{time:{value:0.0} //立方体位于原点,y的最小值是-30.0,而不是0.0},//顶点着色器对象vertexShadervertexShader: vertexShader,// 片元着色器对象fragmentShaderfragmentShader: fragmentShader});export const model = new THREE.Mesh(geometry, material);
javascript">// 渲染循环
function render() {material.uniforms.time.value += 0.5;if(material.uniforms.time.value>10.0){material.uniforms.time.value = 0.0;}renderer.render(scene, camera);requestAnimationFrame(render);
}


http://www.ppmy.cn/embedded/170339.html

相关文章

力扣1594. 矩阵的最大非负积

力扣1594. 矩阵的最大非负积 题目 题目解析及思路 题目要求返回从左上到右下的最大非负积&#xff0c;本题和简单图dp的区别就是出现了负数 若grid[i][j] > 0则和简单图dp一致&#xff0c;dp[i][j] max(dp[i-1][j],dp[i][j-1]) * grid[i][j] 若grid[i][j] < 0则分两…

自学微信小程序的第十天

DAY10 1、调用wx.login()方法获取用户登录凭证code,然后将它发送给开发者服务器。 表43:wx.login()方法的常用选项 选项 类型 说明 timeout number 超时时间,单位为毫秒 success function 调用成功的回调函数 fail function 调用失败的回调函数 complete function 调用结束…

XMOS推出“免开发固件方案”将数字接口音频应用的开发门槛大幅降低

使用该套“免开发固件方案”可将开发周期从三个月缩短到14天 中国深圳&#xff0c;2025年3月——全球领先的软件定义系统级芯片&#xff08;SoC&#xff09;开发商XMOS宣布&#xff1a;公司已推出了“免开发固件方案”&#xff0c;可实现中高端音频解决方案的0代码开发。与传统…

MySQL执行更新SQL流程

目录 1 redo log 2 binlog 3 Update执行逻辑 1 redo log InnoDB引擎特有日志MySQL的WAL&#xff08;Writing Ahead logging&#xff09;技术&#xff0c;预写式日志&#xff0c;先写日志再写磁盘当有一条记录需要更新时&#xff0c;InnoDB引擎就会先把记录写在redo log日志中&a…

代码随想录算法训练营day49(0217)

单调栈的收尾&#xff0c;接雨水很常考 1.接雨水 题目 42. 接雨水 给定 n 个非负整数表示每个宽度为 1 的柱子的高度图&#xff0c;计算按此排列的柱子&#xff0c;下雨之后能接多少雨水。 示例 1&#xff1a; 输入&#xff1a;height [0,1,0,2,1,0,1,3,2,1,2,1] 输出&…

Collab-Overcooked:专注于多智能体协作的语言模型基准测试平台

2025-02-27&#xff0c;由北京邮电大学和理想汽车公司联合创建。该平台基于《Overcooked-AI》游戏环境&#xff0c;设计了更具挑战性和实用性的交互任务&#xff0c;目的通过自然语言沟通促进多智能体协作。 一、研究背景 近年来&#xff0c;基于大型语言模型的智能体系统在复…

[密码学实战]Java实现国密(SM2)密钥协商详解:原理、代码与实践

一、代码运行结果 二、国密算法与密钥协商背景 2.1 什么是国密算法&#xff1f; 国密算法是由中国国家密码管理局制定的商用密码标准&#xff0c;包括&#xff1a; SM2&#xff1a;椭圆曲线公钥密码算法&#xff08;非对称加密/签名/密钥协商&#xff09;SM3&#xff1a;密码…

基于微信小程序的停车场管理系统的设计与实现

第1章 绪论 1.1 课题背景 随着移动互联形式的不断发展&#xff0c;各行各业都在摸索移动互联对本行业的改变&#xff0c;不断的尝试开发出适合于本行业或者本公司的APP。但是这样一来用户的手机上就需要安装各种软件&#xff0c;但是APP作为一个只为某个公司服务的一个软件&a…