three.js通过自定义着色器实现智慧城市扫光效果

前言:

在一些智慧城市的项目中我们经常看到一些酷炫的扫光效果、各个方向的流动光线、半透明发光的电子围栏等等,给人一种赛博朋克的感觉。那这种酷炫的是怎么实现的呢?本篇文章主要讲一下扫光效果的实现;很明显这种扫光没有办法通过three.js自带的灯光实现,因为没有这些奇形怪状的灯啊,那就只能通过着色器材质实现了。

我们先看实现效果

这里直截了两张图,不过扫光全过程也可以想得到,就是光从中心点向外扩散一个具有宽度的圆环,被圆环光扫到的box和地板会被光照亮,我这里为了效果明显一些把光的颜色和强度调的很显眼,实际可以自己的审美调节一下。

实现思路:

问题一:什么东西需要参与扫光效果

我们可以看到主要是场景中的建筑物和地板参与了扫光效果,那就确定了这些物体需要单独写着色器材质

问题二:如何做到灯光同步

我们可以看到扫光的时候,扫光的圆半径不断扩大,到一定的半径后就变成消失了;那这两种材质要如何保持同步呢?

问题三:如何确定那些片元被灯光照到了?

我们在shadertoy等网站看到的一些demo都是通过uv坐标去着色的,我们是否可以通过uv坐标照到被灯光照到的片元或者顶点?如果不行还有没有别的办法

如果把这些疑问都解决了,那答案就会浮出水面了

我们边看代码边分析,场景初始化什么的就不给出来了,都是一样的,先随机生成一些box用来模拟智慧城市里面的建筑物和生成地板

	const getRandom = () => {
		return (-Math.random() * 1500 + 1500) - (Math.random() * 1500)
	}

	// 随机生成box
	for (let i = 0; i < 800; i++) {
		const height = Math.floor(Math.random() * 70 + 30)
		const boxGeo = new THREE.BoxGeometry(Math.floor(Math.random() * 30 + 10),height , Math.floor(Math.random() * 30 + 10))
		const boxMesh = new THREE.Mesh(boxGeo, shaderMat)
		boxMesh.position.copy(new THREE.Vector3(getRandom(), height / 2, getRandom()))
		scene.add(boxMesh)
	}
    // 添加地板
	const goundGeo = new THREE.PlaneGeometry(5000, 5000)
	const ground = new THREE.Mesh(goundGeo, planeMat)
	ground.rotateX(-Math.PI / 2)
	scene.add(ground)

我们先看看这里面的planeMatshaderMat要如何实现

planeMat


			varying vec2 vUv;
			varying vec4 v_position;
			varying vec3 v_normal;
	
			uniform float innerCircleWidth;
			uniform float circleWidth;
			uniform vec3 center;
			uniform vec3 color;

            uniform sampler2D texture1;
	
			void main() {

				float dis = length(v_position.xyz - center);
				vec4 buildingColor = vec4(0.2,0.3,0.4,0.6);
                vec4 textureColor = texture(texture1,vUv);

				vec4 lightColor = vec4(0.2);
				vec3 lightDir = vec3(1.0,1.0,0.5);
				float c = dot(lightDir,v_normal);
                float r = 1.0- smoothstep(50.0,800.0,dis);

				float col = smoothstep(innerCircleWidth-circleWidth,innerCircleWidth,dis) - smoothstep(innerCircleWidth,innerCircleWidth+circleWidth,dis);
				vec4 scanColor = mix(buildingColor * r,vec4(color, 1.0),col);
				scanColor += lightColor*c + vec4(0.05);

				gl_FragColor = scanColor + vec4(textureColor.xyz * 2.5, 1.0);
			}
			

在代码中我们得到了问题三答案;首先我们在片元着色器接收了来自顶点着色器的v_position,然后通过glsl的length得到了场景中对象片元的世界坐标到中点(可以自定义)的距离。这个是整个过程中最关键,有了这个之后我们就可以用距离方程做很多有趣的东西,比如雷达扩散效果或者结合perlin噪音的随机制作一些波纹等等效果;这里我们主要是做扫光效果;那如果我不想从要这种圆的扩散效果,我想要矩形的从左到右或再从右到左再或者各种奇葩的效果怎么办呢?别怕,只需要通过距离照到要着色的片元,然后进行着色就可以了,一点也不慌

由于纯颜色看起来太单调,我加了一点纹理,也就是夜景夜景效果可以把材质用在box的除了顶部和底部的各个面看起来会更加好看,但由于不是这篇文章的重点就不说了,具体怎么实现,可以去看看文档;如何给不同的面指定不同的材质

里面还模拟了一下,聚光灯(从上往下,距离方程实现)、环境光、和平行光(点乘顶点法线和光的方向,注意向量归一化和点乘后的取值范围,因为背面是照不到的)的效果,环境光让整体有一点亮度,平行光让对象表面不同的明暗对比,更加有立体感,最后是聚光灯主要是突出我们强调的对象,这里就是我们的中心点啦,当然这些都不是我们的重点就展开说了,如果感兴趣后面再出一篇文章单独展开说明

顺便提一点,我为什么用mix而不像写前端代码那样用if else 那些执行代码,那是因为这些自带的glsl函数被称为魔法函数,是具有硬件加速效果的;运行的速度会更快、执行效率更高

shaderMat

			varying vec2 vUv;
			varying vec3 v_position;
	
			uniform float innerCircleWidth;
			uniform float circleWidth;
			uniform float opacity;
			uniform vec3 center;
		
			uniform vec3 color;
			uniform vec3 diff;

			bool hex(vec2 p) {
				p.x *= 0.57735*2.0;
				p.y += mod(floor(p.x), 2.0)*0.5;
				p = abs((mod(p, 1.0) - 0.5));
				return abs(max(p.x*1.5 + p.y, p.y*2.0) - 1.0) > 0.05;
		    }

			void main() {

				float dis = length(v_position - center);

				bool h = hex(vUv*100.0);
				float col = smoothstep(innerCircleWidth-circleWidth,innerCircleWidth,dis) - smoothstep(innerCircleWidth,innerCircleWidth+circleWidth,dis);
				vec4 finalColor = 1.0- mix(vec4(0.9),vec4(color, opacity),col);
				float r = 1.0- smoothstep(50.0,1000.0,dis);
		
				float hh;
				if(h){
					hh = float(h);
					gl_FragColor = finalColor + vec4(hh) * r * 0.6 + (1.0-r) * vec4(vec3(0.001),1.0);
				}else{
					gl_FragColor = vec4(0.0);     
				}
			}

地板的材质扫光效果的核心上面一样,也是通过距离方程实现的,上面主要是叠加了扫光、聚光灯和六边形,因为只有扫光的效果看起来有点单调,本来想找一点地板的纹理,但想了一下,我们要的是科技风!柯基峰!科技疯。我就加了六边形的效果,一看就很柯基疯,这不是重点就不展开说了

那说完了我好像没有回到问题二啊,如何同步,别急,别急,我知道你很急,但是你先别急啊

看完完整代码就会明白了,直接上代码

class ScanMat {
  shaderMat: THREE.ShaderMaterial;
  planeMat: THREE.ShaderMaterial;
  updateScan: () => void;

  radius: number;
  width: number;
  constructor(width: number, radius: number) {
    this.radius = radius;
    this.width = width;
    const texture = new THREE.TextureLoader().load('2.png')
    this.shaderMat = new THREE.ShaderMaterial({
      uniforms: {
        innerCircleWidth: {
          value: 0,
        },
        circleWidth: {
          value: width,
        },
        color: {
          value: new THREE.Color(0.8, 0.85, 0.9),
        },
        center: {
          value: new THREE.Vector3(0, 0, 0),
        },
        texture1: {
          value: texture,
        },
      },
      vertexShader: `
			varying vec2 vUv;
			varying vec4 v_position;
			varying vec3 v_normal;

			void main() {
				vUv = uv;
				v_position = modelMatrix * vec4(position, 1.0);
				v_normal = normal;
				gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
			}
			`,
      fragmentShader: `
			varying vec2 vUv;
			varying vec4 v_position;
			varying vec3 v_normal;
	
			uniform float innerCircleWidth;
			uniform float circleWidth;
			uniform vec3 center;
			uniform vec3 color;

      uniform sampler2D texture1;
	
			void main() {

				float dis = length(v_position.xyz - center);
				vec4 buildingColor = vec4(0.2,0.3,0.4,0.6);
        vec4 textureColor = texture(texture1,vUv);

				vec4 lightColor = vec4(0.2);
				vec3 lightDir = vec3(1.0,1.0,0.5);
				float c = dot(lightDir,v_normal);
        float r = 1.0- smoothstep(50.0,800.0,dis);

				float col = smoothstep(innerCircleWidth-circleWidth,innerCircleWidth,dis) - smoothstep(innerCircleWidth,innerCircleWidth+circleWidth,dis);
				vec4 scanColor = mix(buildingColor * r,vec4(color, 1.0),col);
				scanColor += lightColor*c + vec4(0.05);

				gl_FragColor = scanColor + vec4(textureColor.xyz * 2.5, 1.0);
			}
			`
    });

    this.planeMat = new THREE.ShaderMaterial({
      uniforms: {
        innerCircleWidth: {
          value: 0,
        },
        circleWidth: {
          value: width,
        },
        diff: {
          value: new THREE.Color(0.2, 0.2, 0.2),
        },
        color: {
          value: new THREE.Color(0.8),
        },
        opacity: {
          value: 0.9,
        },
        center: {
          value: new THREE.Vector3(0, 0, 0),
        },
      },
      vertexShader: `
			varying vec2 vUv;
			varying vec3 v_position;
			void main() {
				vUv = uv;
				v_position = position;
				gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
			}
			`,
      fragmentShader: `
			varying vec2 vUv;
			varying vec3 v_position;
	
			uniform float innerCircleWidth;
			uniform float circleWidth;
			uniform float opacity;
			uniform vec3 center;
		
			uniform vec3 color;
			uniform vec3 diff;

			bool hex(vec2 p) {
				p.x *= 0.57735*2.0;
				p.y += mod(floor(p.x), 2.0)*0.5;
				p = abs((mod(p, 1.0) - 0.5));
				return abs(max(p.x*1.5 + p.y, p.y*2.0) - 1.0) > 0.05;
		    }

			void main() {

				float dis = length(v_position - center);

				bool h = hex(vUv*100.0);
				float col = smoothstep(innerCircleWidth-circleWidth,innerCircleWidth,dis) - smoothstep(innerCircleWidth,innerCircleWidth+circleWidth,dis);
				vec4 finalColor = 1.0- mix(vec4(0.9),vec4(color, opacity),col);
				float r = 1.0- smoothstep(50.0,1000.0,dis);
		
				float hh;
				if(h){
					hh = float(h);
					gl_FragColor = finalColor + vec4(hh) * r * 0.6 + (1.0-r) * vec4(vec3(0.001),1.0);
				}else{
					gl_FragColor = vec4(0.0);     
				}
			}
			`,
      transparent: true,
    });

    this.updateScan = () => {
      this.shaderMat.uniforms.innerCircleWidth.value += 5;
      if (this.shaderMat.uniforms.innerCircleWidth.value > 2000) {
        this.shaderMat.uniforms.innerCircleWidth.value = -this.radius;
      }
      this.planeMat.uniforms.innerCircleWidth.value += 5;
      if (this.planeMat.uniforms.innerCircleWidth.value > 2000) {
        this.planeMat.uniforms.innerCircleWidth.value = -this.radius;
      }
    };
  }
}

最后别忘了在每一帧里调用,update方法,不然灯光是动不起来的,快去自己动手试一下吧

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
### 回答1: three.js 是一个开源的JavaScript 3D图形库,它非常强大且灵活,可以用于创建各种各样的3D效果。在three.js中,我们可以通过一些技巧和特效来实现城市、波浪、扫光和掠过效果。 首先,要实现城市效果,可以使用three.js中的几何体和纹理功能来创建楼房和道路。可以通过设置不同的材质和贴图来使建筑物具有真实的外观。还可以通过在场景中放置适当的灯光来增强城市的氛围,例如环境光和点光源。 接下来,实现波浪效果可以使用three.js中的顶点着色器和片元着色器。通过改变顶点的位置和颜色,可以使表面看起来像波浪一样起伏。可以使用简单的数学函数来模拟波浪的运动,例如正弦函数。通过调整波浪的振幅和频率,可以创建出不同形状的波浪效果。 然后,要实现扫光效果,可以使用three.js中的相机功能和动画来模拟扫光效果。可以通过改变相机的位置和视角来创建扫过整个城市的效果。同时,可以使用three.js中的渲染器功能来确保扫光效果能够正确地显示在屏幕上。 最后,要实现掠过效果,可以使用three.js中的交互功能和事件监听器。通过监听鼠标或触摸事件,可以控制相机的移动和视角变化,实现掠过城市的效果。可以使用Tween.js来创建平滑的过渡动画,使相机的移动更加流畅。 综上所述,借助three.js的强大功能和灵活性,我们可以实现城市、波浪、扫光和掠过效果。通过合理运用各种技巧和特效,可以创造出令人惊叹的动态场景。 ### 回答2: three.js 是一种使用 JavaScript 实现的 3D 图形库,可用于创建复杂的城市场景。对于实现波浪特效,可以使用 three.js 中的 ShaderMaterial 来创建水的表面并在水面上添加波浪效果着色器。 首先,我们需要创建一个平面几何体来表示水的表面。然后,使用 ShaderMaterial 将自定义着色器应用于这个几何体。在着色器中,我们可以通过对顶点进行位移来模拟水的波动效果。可以通过修改着色器中的时间变量来实现动态的波浪效果。 在城市场景中添加波浪特效后,我们可以进一步实现城市扫光效果。城市扫光是指在城市的街道或建筑物上移动的光束,形成一种动态的光影效果。 为了实现城市扫光效果,我们可以使用 three.js 中的光源和材质来创建一个适当的光束。然后,使用动画循环更新光束的位置以模拟扫光效果。可以通过修改光源的位置和强度来调整扫光的方向和明亮度,以达到所需的视觉效果。 最后,我们还可以实现城市中掠过的效果。掠过效果意味着在城市的建筑物或街道上快速移动的光影效果。可以通过在场景中添加一个或多个掠过的光源对象,并使用动画循环来更新它们的位置来实现掠过效果。可以设置光源的颜色、形状和掠过速度等参数来调整掠过效果的外观。 综上所述,利用 three.js 可以很好地实现城市场景中的波浪特效、城市扫光和掠过效果。通过合理地使用几何体、着色器、光源和材质等功能,可以创造出逼真且令人赞叹的城市景观。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值