这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助

建模

首先我们需要一些贴图素材

贴图素材一般可以在3dtextures网站上找到,这里我找了2份,包含了墙的法线贴图和潮湿地面的法线、透明度、粗糙度贴图

通过kokomi.AssetManager将贴图素材一次性全部加载出来,将它们应用到Mesh上,加上基本的环境光照,即可完成最基本的建模

// 光照
const pointLight1 = new THREE.PointLight(config.color, 0.5, 17, 0.8);
pointLight1.position.set(0, 2, 0);
this.scene.add(pointLight1);
... // 网格
const aspTex = am.items["asphalt-normal"];
aspTex.rotation = THREE.MathUtils.degToRad(90);
aspTex.wrapS = aspTex.wrapT = THREE.RepeatWrapping;
aspTex.repeat.set(5, 8); const wallMat = new THREE.MeshPhongMaterial({
color: new THREE.Color("#111111"),
normalMap: aspTex,
normalScale: new THREE.Vector2(0.5, 0.5),
shininess: 200,
}); const wall = new THREE.Mesh(new THREE.BoxGeometry(25, 20, 0.5), wallMat);
this.scene.add(wall);
wall.position.y = 10;
wall.position.z = -10.3;
... // 文字
const t3d = new kokomi.Text3D(this, config.text, font, {
size: 3,
height: 0.2,
curveSegments: 120,
bevelEnabled: false,
});
t3d.mesh.geometry.center(); const tm = new THREE.Mesh(
t3d.mesh.geometry,
new THREE.MeshBasicMaterial({
color: config.color,
})
);
this.scene.add(tm);
tm.position.y = 1.54;

积水地面

地面上的积水能反射出周围的景色,因此我们将选用kokomi.Reflector来实现反射效果

const mirror = new kokomi.Reflector(new THREE.PlaneGeometry(25, 100));
mirror.position.z = -25;
mirror.rotation.x = -Math.PI / 2;

普通的反射器仅仅是一面镜子,因此我们要自定义反射器的Shader

涟漪效果

之前逛shadertoy时看到了一个很棒的涟漪特效,就直接拿来用了

// https://www.shadertoy.com/view/4djSRW
float hash12(vec2 p){
vec3 p3=fract(vec3(p.xyx)*.1031);
p3+=dot(p3,p3.yzx+19.19);
return fract((p3.x+p3.y)*p3.z);
} vec2 hash22(vec2 p){
vec3 p3=fract(vec3(p.xyx)*vec3(.1031,.1030,.0973));
p3+=dot(p3,p3.yzx+19.19);
return fract((p3.xx+p3.yz)*p3.zy);
} // https://gist.github.com/companje/29408948f1e8be54dd5733a74ca49bb9
float map(float value,float min1,float max1,float min2,float max2){
return min2+(value-min1)*(max2-min2)/(max1-min1);
} vec2 rippleUv=75.*p*uTexScale; vec2 p0=floor(rippleUv); float rainStrength=map(uRainCount,0.,10000.,3.,.5);
if(rainStrength==3.){
rainStrength=50.;
} vec2 circles=vec2(0.);
for(int j=-MAX_RADIUS;j<=MAX_RADIUS;++j)
{
for(int i=-MAX_RADIUS;i<=MAX_RADIUS;++i)
{
vec2 pi=p0+vec2(i,j);
#if DOUBLE_HASH
vec2 hsh=hash22(pi);
#else
vec2 hsh=pi;
#endif
vec2 p=pi+hash22(hsh); float t=fract(.8*iTime+hash12(hsh));
vec2 v=p-rippleUv;
float d=length(v)-(float(MAX_RADIUS)+1.)*t+(rainStrength*.1*t); float h=1e-3;
float d1=d-h;
float d2=d+h;
float p1=sin(31.*d1)*smoothstep(-.6,-.3,d1)*smoothstep(0.,-.3,d1);
float p2=sin(31.*d2)*smoothstep(-.6,-.3,d2)*smoothstep(0.,-.3,d2);
circles+=.5*normalize(v)*((p2-p1)/(2.*h)*(1.-t)*(1.-t));
}
}
circles/=float((MAX_RADIUS*2+1)*(MAX_RADIUS*2+1)); float intensity=.05*floorOpacity;
vec3 n=vec3(circles,sqrt(1.-dot(circles,circles))); vec2 rainUv=intensity*n.xy;

与地面结合

光有涟漪效果也不够,要将它与地面的贴图相结合起来

这里采用了自定义mipmap技术,利用kokomi.PackedMipMapGenerator生成了多个贴图的mipmap

自定义mipmap除了能捆绑贴图外,还有个好处就是可以动态控制贴图的模糊程度

const mipmapper = new kokomi.PackedMipMapGenerator();
const mirrorFBO = mirror.getRenderTarget();
const mipmapFBO = new kokomi.FBO(this);
mirror.material.uniforms.tDiffuse.value = mipmapFBO.rt.texture;
this.update(() => {
mipmapper.update(mirrorFBO.texture, mipmapFBO.rt, this.renderer);
});
vec2 p=vUv;
vec2 texUv=p*uTexScale;
texUv+=uTexOffset;
float floorOpacity=texture(uOpacityTexture,texUv).r;
vec3 floorNormal=texture(uNormalTexture,texUv).rgb*2.-1.;
floorNormal=normalize(floorNormal);
float roughness=texture(uRoughnessTexture,texUv).r; vec2 finalUv=reflectionUv+floorNormal.xy*uDistortionAmount-rainUv; float level=roughness*uBlurStrength; vec3 col=packedTexture2DLOD(tDiffuse,finalUv,level,uMipmapTextureSize).rgb;

下雨动画

生成雨滴

雨滴数量会很多,因此要用到THREE.InstancedMesh来生成实例化网格对象

const rain = new THREE.InstancedMesh(new THREE.PlaneGeometry(), rainMat, count);
rain.instanceMatrix.needsUpdate = true; const dummy = new THREE.Object3D(); for (let i = 0; i < rain.count; i++) {
dummy.position.set(
THREE.MathUtils.randFloat(-10, 10),
0,
THREE.MathUtils.randFloat(-20, 10)
);
dummy.scale.set(0.03, THREE.MathUtils.randFloat(0.3, 0.5), 0.03);
dummy.updateMatrix();
rain.setMatrixAt(i, dummy.matrix);
}
rain.rotation.set(-0.1, 0, 0.1);
rain.position.set(0, 4, 4);

这里要注意一点:雨滴的方向是始终朝向用户的,为了达成这点就要用billboard方案来实现

vec3 billboard(vec3 v,mat4 view){
vec3 up=vec3(view[0][1],view[1][1],view[2][1]);
vec3 right=vec3(view[0][0],view[1][0],view[2][0]);
vec3 pos=right*v.x+up*v.y;
return pos;
} vec3 billboardPos=billboard(transformed,modelViewMatrix);
transformed=billboardPos;

下落动画

我们可以给雨滴赋予随机的高度和速度attribute,并在顶点着色器中让它动起来

const progressArr = [];
const speedArr = []; for (let i = 0; i < rain.count; i++) {
... progressArr.push(Math.random());
speedArr.push(dummy.scale.y * 10);
} rain.geometry.setAttribute(
"aProgress",
new THREE.InstancedBufferAttribute(new Float32Array(progressArr), 1)
);
rain.geometry.setAttribute(
"aSpeed",
new THREE.InstancedBufferAttribute(new Float32Array(speedArr), 1)
);
attribute float aProgress;
attribute float aSpeed; uniform float uSpeed;
uniform float uHeightRange; vec3 distort(vec3 p){
float y=mod(aProgress-iTime*aSpeed*.25*uSpeed,1.)*uHeightRange-(uHeightRange*.5);
p.y+=y;
return p;
} transformed=distort(transformed);

反射效果

创建背景的离屏渲染FBO,将其作为反射的主要材质

const bgFBO = new kokomi.FBO(this, {
width: window.innerWidth * 0.1,
height: window.innerHeight * 0.1,
});
rainMat.uniforms.uBgRt.value = bgFBO.rt.texture; const fboCamera = this.camera.clone(); this.update(() => {
rain.visible = false;
this.renderer.setRenderTarget(bgFBO.rt);
this.renderer.render(this.scene, fboCamera);
this.renderer.setRenderTarget(null);
rain.visible = true;
});

在顶点着色器中获取屏幕空间vScreenspace

// https://github.com/Samsy/glsl-screenspace
vec2 screenspace(mat4 projectionmatrix,mat4 modelviewmatrix,vec3 position){
vec4 temp=projectionmatrix*modelviewmatrix*vec4(position,1.);
temp.xyz/=temp.w;
temp.xy=(.5)+(temp.xy)*.5;
return temp.xy;
} vScreenspace=screenspace(projectionMatrix,modelViewMatrix,transformed);

在片元着色器中采样反射材质

uniform sampler2D uNormalTexture;
uniform sampler2D uBgRt;
uniform float uRefraction;
uniform float uBaseBrightness; varying vec2 vScreenspace; void main(){
vec2 p=vUv; vec4 normalColor=texture(uNormalTexture,p); if(normalColor.a<.5){
discard;
} vec3 normal=normalize(normalColor.rgb); vec2 bgUv=vScreenspace+normal.xy*uRefraction;
vec4 bgColor=texture(uBgRt,bgUv); float brightness=uBaseBrightness*pow(normal.b,10.); vec3 col=bgColor.rgb+vec3(brightness); col=vec3(p,0.); gl_FragColor=vec4(col,1.);
}

这里有一点要注意:积水地面中要把雨滴的反射去掉,不然会看着很乱

rainFloor.mirror.ignoreObjects.push(rain);

灯光闪烁

setInterval来间歇地设置文字和灯光材质的颜色即可

// flicker
const turnOffLight = () => {
tm.material.color.copy(new THREE.Color("black"));
pointLight1.color.copy(new THREE.Color("black"));
}; const turnOnLight = () => {
tm.material.color.copy(new THREE.Color(config.color));
pointLight1.color.copy(new THREE.Color(config.color));
}; let flickerTimer = null; const flicker = () => {
flickerTimer = setInterval(async () => {
const rate = Math.random();
if (rate < 0.5) {
turnOffLight();
await kokomi.sleep(200 * Math.random());
turnOnLight();
await kokomi.sleep(200 * Math.random());
turnOffLight();
await kokomi.sleep(200 * Math.random());
turnOnLight();
}
}, 3000);
}; flicker();

后期处理

为了让文字灯光看上去更加明亮,可以用Bloom滤镜来照亮文字

由于后期处理中原先renderer的抗锯齿会失效,故用SMAA滤镜来实现抗锯齿

// postprocessing
const composer = new POSTPROCESSING.EffectComposer(this.renderer);
this.composer = composer; composer.addPass(new POSTPROCESSING.RenderPass(this.scene, this.camera)); // bloom
const bloom = new POSTPROCESSING.BloomEffect({
luminanceThreshold: 0.4,
luminanceSmoothing: 0,
mipmapBlur: true,
intensity: 2,
radius: 0.4,
});
composer.addPass(new POSTPROCESSING.EffectPass(this.camera, bloom)); // antialiasing
const smaa = new POSTPROCESSING.SMAAEffect();
composer.addPass(new POSTPROCESSING.EffectPass(this.camera, smaa));

待优化

效果算是基本实现了,但也有很多待优化的点

  1. 添加现实中的雨声
  2. 实现更棒的相机交互
  3. 添加更多的物体

本文转载于:

https://juejin.cn/post/7200443454567137336

如果对您有所帮助,欢迎您点个关注,我会定时更新技术文档,大家一起讨论学习,一起进步。

记录--用three.js渲染真实的下雨效果的更多相关文章

  1. vue.js 渲染完成回调

    vue.js渲染完成后,想触发一些事情,写在哪里呢? 答案是mounted 例子: new Vue({ el:'#demo', data:{ text:'Hello' }, mounted:funct ...

  2. js验证真实姓名与身份证号,手机号

    最近的项目中用的需要调用实名认证的接口,实名认证接口价格相比短信而言高了不是几分钱,所以说调用实名认证的条件就要严格把关,因此用到js验证真实姓名与js验证身份证号. 进入正题 1.js验证真实姓名 ...

  3. 手把手教你写电商爬虫-第四课 淘宝网商品爬虫自动JS渲染

    版权声明:本文为博主原创文章,未经博主允许不得转载. 系列教程: 手把手教你写电商爬虫-第一课 找个软柿子捏捏 手把手教你写电商爬虫-第二课 实战尚妆网分页商品采集爬虫 手把手教你写电商爬虫-第三课 ...

  4. js渲染的3d玫瑰

    参看下面链接: 程序员最美情人节礼物:JS渲染的3D玫瑰

  5. 使用Three.js渲染Sketchup导出的dae

    打算做个轮盘游戏,直接上3D吧. 第一步:制作模型 3DMax和Maya下载和破解比较麻烦, 就用之前的Sketchup来试试吧. 最后效果图: 俯视图 仰视图 制作步骤: 1 先画一个圆 2 从圆心 ...

  6. 关于js渲染网页时爬取数据的思路和全过程(附源码)

    于js渲染网页时爬取数据的思路 首先可以先去用requests库访问url来测试一下能不能拿到数据,如果能拿到那么就是一个普通的网页,如果出现403类的错误代码可以在requests.get()方法里 ...

  7. python 爬取世纪佳缘,经过js渲染过的网页的爬取

    #!/usr/bin/python #-*- coding:utf-8 -*- #爬取世纪佳缘 #这个网站是真的烦,刚开始的时候用scrapy框架写,但是因为刚接触框架,碰到js渲染的页面之后就没办法 ...

  8. 【js 正则表达式】记录所有在js中使用正则表达式的情况

    说实话,对正则表达式有些许的畏惧感,之前的每次只要碰到需要正则表达式去匹配的情况,都会刻意的躲过或者直接从度娘处获取. 此时此刻,感觉到了某一个特定的点去触及她.但笔者对于正则表达式使用上的理解是这样 ...

  9. Java使用HtmlUnit抓取js渲染页面

    需求: 需要采集js渲染的页面,有些网站的页面是js渲染的 实现: 基于HtmlUnit实现: public static void getAjaxPage() throws Exception{ W ...

  10. java_爬虫_获取经过js渲染后的网页源码

    md 弄了一天了……(这个月不会在摸爬虫了,浪费生命) 进入正题: 起初是想写一个爬虫来爬一个网站的视频,但是怎么爬取都爬取不到,分析了下源代码之后,发现源代码中并没有视频的dom 但是在浏览器检查元 ...

随机推荐

  1. Python实现冒泡排序、选择排序、插入排序

    排序与搜索 排序算法(英语:Sorting algorithm)是一种能将一串数据依照特定顺序进行排列的一种算法. 排序算法的稳定性 稳定性:稳定排序算法会让原本有相等键值的纪录维持相对次序.也就是如 ...

  2. Wireguard笔记(三) lan-to-lan子网穿透和多网段并存

    目录 Wireguard笔记(一) 节点安装配置和参数说明 Wireguard笔记(二) 命令行操作 Wireguard笔记(三) lan-to-lan子网穿透和多网段并存 多 Wireguard 服 ...

  3. 【Unity3D】发射(Raycast)物理射线(Ray)

    1 前言 ​ 碰撞体组件Collider 中介绍了 2 个碰撞体之间的碰撞检测,本文将介绍物理射线与碰撞体之间的碰撞检测.物理射线由 Ray 定义,通过 Physics.Raycast / Physi ...

  4. Kotlin 协程四 —— Flow 和 Channel 的应用

    目录 一. Flow 与 Channel 的相互转换 1.1 Flow 转换为 Channel 1.1.1 ChannelFlow 1.1.2 produceIn -- 将 Flow 转换为单播式 C ...

  5. 逆向实战31——xhs—xs算法分析

    前言 本文章中所有内容仅供学习交流,抓包内容.敏感网址.数据接口均已做脱敏处理,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关,若有侵权,请联系我立即删除! 公众号链接 目标网站 aH ...

  6. 【Azure 应用服务】Web App Service 中的 应用程序配置(Application Setting) 怎么获取key vault中的值

    问题描述 App Service中,如何通过 Application Setting 来配置 Key Vault中的值呢? 问题解答 首先,App Service服务可以直接通过引用的方式,无需代码的 ...

  7. 【Azure Redis 缓存】定位Java Spring Boot 使用 Jedis 或 Lettuce 无法连接到 Redis的网络连通性步骤

    问题描述 Java Spring Boot的代码在IDE里面跑可以连上 Azure 的 Redis服务,打包成Image放在容器里面跑,就连不上azure的redis服务,错误消息为: Unable ...

  8. RocketMQ(9) 消息堆积与消费延迟

    消息堆积与消费延迟 1 概念 消息处理流程中,如果Consumer的消费速度跟不上Producer的发送速度,MQ中未处理的消息会越来越多(进的多出的少),这部分消息就被称为堆积消息.消息出现堆积进而 ...

  9. 你想要一个简单的 MQ 吗?(最简单的那种)

    FolkMQ 一个简单的消息中间件(全球最简单的那种,要比谁都简单!).追世间简单为何物,可叫我生死相许! 面向简单编程 1) 启动服务 docker run -p 18602:18602 -p 86 ...

  10. C 语言字符串操作总结

    C 语言字符串操作总结 一.字符串操作 size_t 是一个无符号整型. 1.1 strcpy 函数原型:char *strcpy(char *dest, const char *src). 功 能: ...