ThreeJS 中体渲染,利用噪声模拟烟,云
ThreeJS 中体渲染,利用噪声模拟烟,云体渲染的东西也看了一段时间了,这里结合Three.js中体积云的例子,实现shdertoy中的一个效果,先放效果图。Fire2 (shadertoy.com), 这里是参考的效果,可以自行参看源码。体渲染,Volume Rendering传统建模方式,可以理解为表面建模,通过构建物体外表面,在三维中展示实际物体。相对的,体渲染是从三维数据中生成图像,典型
ThreeJS 中体渲染,利用噪声模拟烟,云
体渲染的东西也看了一段时间了,这里结合Three.js中体积云的例子,实现shdertoy中的一个效果,先放效果图。
Fire2 (shadertoy.com), 这里是参考的效果,可以自行参看源码。
体渲染,Volume Rendering
传统建模方式,可以理解为表面建模,通过构建物体外表面,在三维中展示实际物体。相对的,体渲染是从三维数据中生成图像,典型的例子就是医疗上的CT。本文中不涉及体渲染中的光学模型,仅是对数据进行采样,上色。同时简化计算,使用的几何体为圆球,当然也可以换成立方体,计算方式不会复杂,之后附带立方体和球体的射线检测。
体渲染在表现自然现象,云、雾、火等,相对于表面建模,或者贴图有很大优势。最大的不同就是,实心的,当然效果也好太多了。
3D纹理,sample3D
本次主要要渲染动态更新的体数据,就不需要提前生成体数据了。通过fragmentshader来对采样的射线进行噪声处理,达到动画效果。
相关算法
噪声
噪声函数相关的内容,可以自行搜索,这里贴一个IQ大神的博客地址。Inigo Quilez :: fractals, computer graphics, mathematics, shaders, demoscene and more (iquilezles.org)。
文中涉及的噪声,分型布朗都是从shadertoy获取,代码仅作适当说明。
立方体射线求交,AABB
先解释一下AABB,Axis-Aligned Bounding Box, 轴对称包围盒。我们在放置立方体时,立方体各边都同三个坐标轴平行,可以极大简化计算。如下图,计算射线 o d od od进入一组平面(可以理解为立方体的两个对立面),计算进入和射出的位置时,可以只采用对应轴的分量即可。如下图右侧的公式。
假设平面P垂直于X轴,则计算过程可以仅考虑x方向的分量。图中 p x ′ p'_x px′为平面p在x轴处的值, o x , d x o_x, d_x ox,dx分别为:射线原点的x值,射线朝向 d i r dir dir在x轴的分量(射线朝向为单位向量时,可以理解为,射线进入出去两个平面的时间)。
球体射线求交
球体就是射线方程同球面方程,利用求根公式解方程组了,不再介绍。
Three.js中的体渲染
基础内容介绍完,先看一下Three提供的体渲染的例子。场景中主要包括天空盒,和承载体渲染结果的立方体。这些不做说明,重点看一下渲染使用的着色器。
渲染立方体的材质要使用RawShaderMaterial
,采用自定义的着色器。
顶点着色器
vertexshader中主要处理相机位置,和相机朝向,用于传入到fragment中。
Three中相机位置为世界坐标,通过模型变换的逆矩阵,将相机位置换算到模型本地坐标中。模型在本地坐标中处于原点位置,可以简化计算。相机朝向通过顶点位置减相机的本地坐标即可得到。
in vec3 position;
uniform mat4 modelMatrix; //模型本地坐标系,转换到世界坐标系
uniform mat4 modelViewMatrix; //模型世界坐标系,转换到相机坐标系空间
uniform mat4 projectionMatrix; //投影矩阵
uniform vec3 cameraPos; //相机位置
out vec3 vOrigin;
out vec3 vDirect
void main() {
//相机空间坐标
vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 );
//相机在模型本地坐标的位置
vOrigin = vec3( inverse( modelMatrix ) * vec4( cameraPos, 1.0 ) ).xyz;
//相机在模型本地坐标的朝向
vDirection = position - vOrigin;
gl_Position = projectionMatrix * mvPosition;
}
片元着色器
相机位置作为射线原点,相机朝向作为射线方向,这条射线将参与后续的体数据的采样。
代码中的hitBox
即为上面提到的AABB方法。得到的vec2,其中x为最近,y为最远。
void main(){
vec3 rayDir = normalize( vDirection );
vec2 bounds = hitBox( vOrigin, rayDir );
// 丢弃立方体外的像素
if ( bounds.x > bounds.y ) discard;
// 当相机位置在立方体内部时,x为负值,射线反向不需要采样,设置为0,即从内部开始采样
bounds.x = max( bounds.x, 0.0 );
// p为射线第一次进入立方体的位置
vec3 p = vOrigin + bounds.x * rayDir;
vec3 inc = 1.0 / abs( rayDir );
float delta = min( inc.x, min( inc.y, inc.z ) );
delta /= steps;
// Nice little seed from
// https://blog.demofox.org/2020/05/25/casual-shadertoy-path-tracing-1-basic-camera-diffuse-emissive/
uint seed = uint( gl_FragCoord.x ) * uint( 1973 ) + uint( gl_FragCoord.y ) * uint( 9277 ) + uint( frame ) * uint( 26699 );
vec3 size = vec3( textureSize( map, 0 ) );
float randNum = randomFloat( seed ) * 2.0 - 1.0;
// 对开始位置进行偏移,可能是为了避免射线在表面这种临界条件吧
p += rayDir * randNum * ( 1.0 / size );
vec4 ac = vec4( base, 0.0 );
// 从射线进入,到射线穿过立方体,依次采样
for ( float t = bounds.x; t < bounds.y; t += delta ) {
float d = sample1( p + 0.5 );
d = smoothstep( threshold - range, threshold + range, d ) * opacity;
float col = shading( p + 0.5 ) * 3.0 + ( ( p.x + p.y ) * 0.25 ) + 0.2;
ac.rgb += ( 1.0 - ac.a ) * d * col;
ac.a += ( 1.0 - ac.a ) * d;
// 采样颜色累积到接近不透明时,停止采样
if ( ac.a >= 0.95 ) break;
p += rayDir * delta;
}
color = ac;
if ( color.a == 0.0 ) discard;
}
main
方法中的shading
可以理解为上色的过程。
修改片元着色器
以上了解了体渲染基本流程后,开始修改片元着色器以期实现shadertoy中的效果。
首先,shadertoy中的造型是基于球体做的,我们修改几何体为球体。添加hitSphere
方法,t0,t1依次为进入和出去的时间。
vec2 hitSphere(vec3 origin,vec3 dir){
float b=dot(dir,origin);
float c=dot(origin,origin)-_SphereRadius*_SphereRadius;
float t0=-b-sqrt(b*b-c);
float t1=-b+sqrt(b*b-c);
t0=max(t0,0.);
return vec2(t0,t1);
}
RayMarch
由于我们需要在shader中实现球体的绘制,因此需要通过射线原点到球体的距离来绘制球体。这里类似于cloud页面中的步进采样。
vec4 rayMarch(vec3 rayOrigin,vec3 rayStep,out vec3 pos)
{
vec4 sum=vec4(0.,0.,0.,0.);
pos=rayOrigin;
for(int i=0;i<_VolumeSteps;i++)
{
vec4 col=volumeFunc(pos);
col.a*=_Density;
col.rgb*=col.a;
sum=sum+col*(1.0-sum.a);
pos+=rayStep;
}
return sum;
}
渲染球体
同时为了简化计算,射线累计的采样次数都为固定值,不再单独计算。
void main(){
vec3 rayDir = normalize( vDirection );
vec2 bounds=hitSphere(vOrigin,rayDir);
if(bounds.y<0.) discard;
vec3 hitPos;
//射线第一次进入球的位置
vec3 p=vOrigin+bounds.x*rayDir;
vec4 col=rayMarch(p,rayDir*_StepSize,hitPos);
color = col;
if ( color.a == 0.0 ) discard;
}
当rayMarch
中的volumeFunc
直接返回射线到球面的距离时,你将能得到非常光滑的球。
接下来就是对这个球进行造型了,噪声函数很多,可以自行测试,添加噪声函数后,可能得到如下,实时计算的噪声,在GTX1650显卡上,还是很流畅的,再低一些就不好使了,卡成PPT:
这个球还是太规则,添加fbm(分形布朗)后,增加不规则程度,即可得到如下:
最后附上全部代码。代码比较乱,放了几种噪声函数,可以自己试试不同的形状,谨慎观看。在线地址
最后贴一个不同的着色效果。
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)