阅读网 购物 网址 万年历 小说 | 三丰软件 天天财富 小游戏
TxT小说阅读器
↓小说语音阅读,小说下载↓
一键清除系统垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放,产品展示↓
佛经: 故事 佛经 佛经精华 心经 金刚经 楞伽经 南怀瑾 星云法师 弘一大师 名人学佛 佛教知识 标签
名著: 古典 现代 外国 儿童 武侠 传记 励志 诗词 故事 杂谈 道德经讲解 词句大全 词句标签 哲理句子
网络: 舞文弄墨 恐怖推理 感情生活 潇湘溪苑 瓶邪 原创 小说 故事 鬼故事 微小说 耽美 师生 内向 易经 后宫 鼠猫 美文
教育信息 历史人文 明星艺术 人物音乐 影视娱乐 游戏动漫 | 穿越 校园 武侠 言情 玄幻 经典语录 三国演义 西游记 红楼梦 水浒传
 
  阅读网 -> 明星艺术 -> 图形学中的高逼格物理模拟动画是如何渲染出来的? -> 正文阅读

[明星艺术]图形学中的高逼格物理模拟动画是如何渲染出来的?

[收藏本文] 【下载本文】
题主最近在做流体模拟方面的探究,很多流体模拟都是基于粒子的,所以我一直都是在opengl中显示粒子状态下模拟的结果。阅读文献时,看到文章中的渲染结果,…
这是一些美丽的风景,你可能觉得是AI或者是建模软件渲染而成。其实是用数学公式堆出来的,没有模型也没有贴图。

0


海上日出


海上日落


近处水景


山顶日出


简化算法下的水和石
静态html页面的实现:点此查看
如果要分析Glsl代码可以看我发布在shadertoy的例子:Cloudy Sunrise on Alps Lake
逐帧录制成的动态风景视频:
B站的1080p低码率差画质版
4K视频下载版
全部程序仅10K,无需任何模型或素材,也无任何外部依赖。而且都是简单的数学计算,所以可以轻易用任何语言复写,甚至不用gpu,只用js逐点画canvas也可以(不过速度就很感人了)。
作为矢量图,理论上它可以在无限放大的同时还能保持无限精细。
其实现方式,可以理解为实现一个函数 f(x,y,time) = ... (x,y是坐标点的位置,time是时间),这个函数返回(x,y)这个像素点的在某个时刻的颜色。通过调用这个函数获得每个像素点的颜色,就可以绘制出令人震撼的风景图像或视频。
通过理解这个函数的实现过程,可以一窥渲染软件的底层逻辑。
如果只对光追比较感兴趣,可以看我的另一个例子珠宝皇冠:magnificent crown,里面光追计算较多,有大理石和金属光泽以及用多重循环计算光线在钻石中的反射


光线追踪下的珠宝皇冠
本人原创作品(主动引用他人的部分会做说明),这里讲述实现过程,但不是教学,不少算法是反复修改出的,未必是很优。
搞这些研究离不开老婆大人支持的4090显卡,多谢老婆??
欢迎转载,请注明作者嚎叫兽:lerrain@gmail.com
下面会一步一步讲述整个开发过程。
一 、 从画一个带光照的球开始
1、html+js最简单易用,从它开始
初期我们先把时间参数隐掉,先画静态的画面,这样f(x,y,time)就先简化成了f(x,y)
先画一个圆,遍历canvas的每个点,调用f(x,y)获得颜色,f函数很简单,就是和零点距离小于1的时候为白,否则为黑

<canvas width="1280" height="720" style="width:1280;height:720" id="test"></canvas>
<script>
    function f(x, y) {
        var px = (x*2-width)/height;
        var py = (y*2-height)/height;
        var col;
        if (px*px+py*py>0.5*0.5) {
            col = [0,0,0];
        } else {
            col = [1,1,1];
        }
        return col;
    }

    const canvas = document.getElementById('test');
    const ctx = canvas.getContext('2d');
    const width = canvas.width;
    const height = canvas.height;
    const imageData = ctx.createImageData(width, height);

    let i = 0;
    for (let y = 0; y < height; y++) {
        for (let x = 0; x < width; x++) {
            var c = f(x, height-y);
            imageData.data[i++] = c[0]*255;
            imageData.data[i++] = c[1]*255;
            imageData.data[i++] = c[2]*255;
            imageData.data[i++] = 255;
        }
    }
    ctx.putImageData(imageData, 0, 0);
</script>

执行效果如下:


距离函数实现最简单的圆
2、 把圆改成有立体感的球
表面亮度是表面法线和光线夹角的余弦值,关于这个介绍比较多不赘述
修改f函数为:

    function normalize(p) { //将p归一化为单位向量,就是把向量长度等比缩放到1
        let len = Math.sqrt(p[0]*p[0]+p[1]*p[1]+p[2]*p[2]);
        return [p[0]/len,p[1]/len,p[2]/len];
    }

    var lt = normalize([1,1,1]); //随便确定一个光照方向
    function drawBall(px, py) {
        var pz = Math.sqrt(1-px*px-py*py); //球得半径是1,z坐标很容易求得,球心是(0,0,0),那么现在的坐标位置就是法线向量
        var tt = px*lt[0]+py*lt[1]+pz*lt[2]; //求法线向量和光照向量的夹角余弦值,其实就是向量点乘,glsl中提供了标准函数叫dot。注意只有两个向量长度都是1的时候才能这么算
        tt = Math.max(0, tt);
        return [tt,tt,tt];
    }

    function f(x, y) {
        var px = (x*2-width)/height;
        var py = (y*2-height)/height;
        var r = 0.5;
        var col;
        if (px*px+py*py>r*r) {
            col = [0.1,0.1,0.1];
        } else {
            px /= r;
            py /= r;
            col = drawBall(px,py);
        }
        return col;
    }

结果如下


带有光照的球
这是在绝对漫反射下的情况,实际可能根据材质不同还有高光。高光就是在法线和光线夹角很小的时候,对光源的近似镜面反射。
加一行代码即可:tt = Math.min(1, tt+Math.pow(tt, 100));
效果如下,覆盖了一点光泽


带有高光的球
这些光照计算非常简便快速,就是游戏中常用的光栅渲染,如果需要纹理贴图,把光强与贴图直接相乘即可。
二 、放射变换下的基本场景
当一个3d场景展示在2d屏幕上时,远小近大,这在游戏或者软件中一般是通过放射矩阵来实现的,我们可以用光线路径的方式用更容易理解的方式复现这种变换。
这里等我哪天画个示意图就好理解了
把屏幕想象成世界里的一扇窗户,窗外的景色发出的光线穿过窗户进入眼睛,如果窗户是一个仅对进入眼睛的光线敏感的感光片(很多光线穿过窗户没有进入眼睛),那么他拍下的画面就是我们要画的画面。
我们要做的事情就是求出窗户这个屏幕上每个像素点的颜色,只考虑进入眼睛的光线,我们反向追踪光线,找出这光线来自外面哪个物体,根据物体的特性求出这光线的颜色就是最终需要的结果。
1、 首先需要求出入眼的光线方向
假定屏幕中心在世界的位置为(0,0,0),屏幕的高度为2,则屏幕的上沿中点为(0,1,0),下沿中点为(0,-1,0)假定眼睛沿着z轴方向看,画面的视角为60度,此时眼睛与屏幕的距离为sqrt(3),眼睛坐标为(0,0,-sqrt(3))
如果像素点是屏幕中心,那么很显然光线就是眼睛看着的方向,是(0,0,-1)。
对于其他任意屏幕像素点,显然z=0,他的空间位置是(x,y,0),那么视野方向就是把(x,y,-sqrt(3))归一化:normalize([x,y,sqrt(3)])

var ro = [0, 0, -Math.sqrt(3)]; //眼睛位置
var ta = [0, 0, 5000]; //眼睛看向的位置

var nl = height / 2; //屏幕高度为2,每个单位长度是多少像素
var xy = [(x - width/2) / nl, (y - height/2) / nl]; //把屏幕像素转换成我们的坐标系,屏幕中点为(0,0)
var rd = normalize([xy[0],xy[1],Math.sqrt(3)]); //视线方向

2、 沿视线方向探测
我们先简单设定一个起伏的地形

function ground(p) { //随便写一个根据(x,z)返回地表高度函数
	return Math.sin(p[0]*0.001) * 500. + Math.cos(p[2]*0.001) * 500. - 1000.; //p[0]是x坐标,p[2]是z坐标,返回地表的高度,人眼的y为0,我们把地表设的低一些
}

由于眼高为y=0,我们设定水平面要低一些,为y=-800
步进探测函数:碰到物体时,返回物体的材质和距离,什么都没碰到返回null

function raymarch(ro, rd) { //从眼睛出发,沿rd方向前进
    for (var f=0; f<10000;) { //最大探测距离10000
        var pos = [ro[0]+rd[0]*f, ro[1]+rd[1]*f, ro[2]+rd[2]*f]; //就是pos=ro+rd*f
        if (pos[1] <= -800.) { //假设水平面是y=-800
            return {
                type: 0, //0代表是碰到了水
                pos: pos,
                far: f
            };
        }

        var y = ground(pos);
        if (pos[1] <= y) {
            return {
                type: 1, //1代表是碰到了土
                pos: pos,
                far: f
            };
        }

        //这次没有碰到东西,就往前挪一点,远的物体更小,所以越远可以挪的越快
        f+=Math.max(1, 0.1*f);  //提低数值可以提高精细度
    }
    return null; //超出了最大探测距离,什么都没碰到
}

3、 修改渲染函数

function f(x, y) {
    var ro = [0, 0, -Math.sqrt(3)];
    var ta = [0, 0, 5000];

    var nl = height / 2; //屏幕高度为2,每个单位长度是多少像素
    var xy = [(x - width/2) / nl, (y - height/2) / nl];
    var rd = normalize([xy[0],xy[1],Math.sqrt(3)]);

    var hit = raymarch(ro, rd);
    var col;
    if (hit != null) {
        if (hit.type == 1) //画土
            col = [0, 0.6, 0.7];
        else
            col = [0.7, 0.7, 0.5]; //画水
    } else {
        col = [0, 0, 1]; //画天空
    }
    return col;
}

执行一下,就得到了一个简单的三维场景,青色的山,黄色的水,蓝色的天


构建一个场景
可以看到锯齿很严重,这是因为探测步进的速度太快,我们把数值调的低一些,画面就会变得比较细腻:f += Math.max(0.1, 0.01*f);
此时会执行速度会变得很慢,要等一会,但结果好了很多,如下:


步长短了,锯齿就不明显
4、 加入光照
算法和球类似,用夹角余弦值即可,唯一的问题是如何求碰撞点得法线。
由于我们可以用ground函数求得任意一点的高度,那么我们可以用碰撞点和碰撞点周围几个点高度的偏差,来计算碰撞点的法线
设一个很小的距离0.01,用x轴向左右偏移0.01的两点高度差为x,再用z轴向左右偏移0.01的两点高度差为z,用两边位置差0.02作为y,(x,y,z)归一化得到法线
算法如下:

function calcNormal(p) {
        return normalize([ground([p[0]-0.01,p[1],p[2]]) - ground([p[0]+0.01,p[1],p[2]]), 0.02, ground([p[0],p[1],p[2]-0.01]) - ground([p[0],p[1],p[2]+0.01])]);
}

有了法线,简单修改一下渲染函数f

var LIGHT = normalize([1,1,1]);
function f(x, y) {
    var ro = [0, 0, -Math.sqrt(3)];
    var ta = [0, 0, 5000];

    var nl = height / 2; //屏幕高度为2,每个单位长度是多少像素
    var xy = [(x - width/2) / nl, (y - height/2) / nl];
    var rd = normalize([xy[0],xy[1],Math.sqrt(3)]);

    var hit = raymarch(ro, rd);
    var col;
    if (hit != null) {
        var nor;
        if (hit.type == 1) {
            nor = calcNormal(hit.pos);
            col = [0.7, 0.7, 0.5];
        } else {
            nor = [0, 1, 0]; //水面的法线垂直向上
            col = [0.0, 0.5, 0.7];
        }
        var lt = Math.max(0, nor[0]*LIGHT[0]+nor[1]*LIGHT[1]+nor[2]*LIGHT[2]);
        col = [col[0]*lt, col[1]*lt, col[2]*lt];
    } else {
        col = [0, 0, 1];
    }
    return col;
}

如果嫌步进小渲染速度太慢,那么可以优化一下步进的算法:由于地面没有90度角拔起的悬崖,都是平滑起伏,那么当探测点和地表高度差很大的时候,可以快速步进:f += 0.001+Math.max((pos[1]-y)*.5, 0.001*f);
就可以得到有简单光照强弱的场景


光照下的场景
4、 变换视角
之前假设视线是沿z轴,但往往我们需要不同的实现角度来看世界。
实现也很简单,假如我们看向另一个方向,实际上就是主视线向量(就是屏幕中心点的视线向量)从(0,0,1)在三维空间旋转了一下,到了新位置。屏幕上其他所有像素的视线向量也旋转同样的角度即可。
代码如下:

function cross(a, b) {  
   return [a[1]*b[2]-a[2]*b[1], a[2]*b[0]-a[0]*b[2], a[0]*b[1]-a[1]*b[0]];  
}  
  
function rotateRD(ro, ta, rd) {  //传入眼睛的位置ro,看向的位置ta,和看向z轴时各个像素对应的rd
   var cw = normalize([ta[0]-ro[0], ta[1]-ro[1], ta[2]-ro[2]]);  
   var cp = [0, 1, 0];  
   var cu = normalize(cross(cw,cp));  
   var cv = normalize(cross(cu,cw));  
   //返回旋转后的rd
   return [cu[0]*rd[0]+cv[0]*rd[1]+cw[0]*rd[2], cu[1]*rd[0]+cv[1]*rd[1]+cw[1]*rd[2], cu[2]*rd[0]+cv[2]*rd[1]+cw[2]*rd[2]]; 
}

把之前的rd调用一下这个函数做一下旋转:rd = rotateRD(ro, ta, rd);
调整了视角后,渲染结果如下


四 、用GPU来实现
由于js的速度变得不可接受,此时可以用GPU来加速
浏览器基本都支持openGL的web版本webGL
我们用webGL画一个矩形,刚好撑满屏幕,然后利用片元着色器来填充这个矩形,把js的代码搬进片元着色器,就可以像canvas那样随意在屏幕上逐点画图。另外为了防止放射变幻带来的问题,我们用正交相机。
对于新手自己写有点麻烦,可以用我的例子,或者直接找AI帮你写 这么问它就行:“我想用html+webGL画一个充满屏幕的矩形,用正交相机,通过片元着色器先把矩形画成红色”
代码刷刷的就出来了,静静的伴随着是大家担心失业的心跳声。
其中片元着色器的代码,就是我们要改的部分。
1、 迁移js代码至片元着色器
片元着色器和我们js画图的方式差不多,就是不断调用这段着色器代码,把屏幕上所有点的颜色算出来。
只是他的xy不是函数参数,而是gl_FragCoord这个全局变量,坐标是像素坐标,需要我们自己转换成我们的空间坐标。我们需要自己在js中把屏幕大小传进去,设变量名为iResolution

const iResolution = gl.getUniformLocation(program, 'iResolution');  
gl.uniform3fv(iResolution, [canvas.width, canvas.height, 1]);

那么glsl中,js那部分转换坐标的代码就需要改成

vec2 p = (2.0*gl_FragCoord.xy-iResolution.xy) / iResolution.y;

着色器提供了很多函数和类型,我们就不用像js一样自己实现和处理了。 比如2维向量vec2,3维向量vec3,点乘dot,归一化normalize等等,可以自己去找关于glsl的基础文章了解
程序没有什么实际改动,所以结果和前面是完全一样的,就不放了。
2、 鼠标调整视角
由于GPU速度很快,我们可以考虑用鼠标实时调整角度,随时查看渲染结果。
我们把鼠标的xy坐标,像iResolution一样传递给着色器
初始化的时候获取变量

var iMouse = gl.getUniformLocation(program, 'iMouse');

鼠标移动的时候修改参数

var mouse;
function draw(t) { //重画函数
   if (mouse != null) //鼠标有动作就传递给shader
       gl.uniform4f(iMouse, mouse[0], mouse[1], mouse[2], mouse[3]);
   gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
}

window.addEventListener("mousedown", e => {
   mouse = [0,0,1,1];
});
window.addEventListener("mousemove", e => {
   if (mouse != null) {
       mouse[0] = -e.pageX/window.innerWidth*canvas.width*2;
       mouse[1] = -e.pageY/window.innerHeight*canvas.height*2;
       requestAnimationFrame(draw); //鼠标有动作就请求重画
   }
});
window.addEventListener("mouseup", e => {
   mouse = null;
});

完整的静态html页面例子,点此查看,拖拽可以改变视角
五 、噪声模拟地形
用了GPU计算,就可以上一些更复杂的运算来提高真实度。其中噪声函数是最常见的,噪声函数可以理解为渐变的随机函数。
噪声函数和随机函数的区别,看以下两图就能理解
等我去找两张图
我们应该使用伪随机,伪随机是为了保证每次生成模型时的一致性:比如求x,z的点的高度y,渲染需要对每个像素点并发调用f函数,我希望f函数每次调用高度函数的时候,对于同一个地点的高度,每次结果都必须是一样的,否则结果会完全错乱。
原理文章很多,不再赘述,一个典型的2维噪声函数代码如下,属于基础函数了,代码满天飞,谁写的我也不知道。

float n2d(vec2 p) {
   vec2 i = floor(p); p -= i;
   p *= p*(3. - p*2.);
   return dot(mat2(fract(sin(mod(vec4(0, 1, 113, 114) + dot(i, vec2(1, 113)), 6.2831853))*43758.5453))*vec2(1. - p.y, p.y), vec2(1. - p.x, p.x));
}

利用这个代替地形函数

float height(vec2 p) {  
   return (n2d(p*0.002) - .5) * 1000. - 100.;  //可以自定义视角后,水平面调到y=0了
}

执行结果如下:


简单噪声地形
看起来也没感觉好多少,因为太过光滑了,别急,FBM(Fractional Brownian Motion,分形布朗运动)可以解决这个问题。其基础公式如下:
fbm(p)=&#x2211;i=0n&#x2212;1Ai&#x2217;noise(fi&#x2217;p)" role="presentation">fbm(p)=∑i=0n?1Ai?noise(fi?p)fbm(p) = \sum_{i=0}^{n-1}A_i*noise(f_i*p)
简单说就是把多个周期不一样的噪声叠加。最常见的是每次叠加频率翻倍,结果减半,叠加N次后效果就不一样了,代码如下:

float fbm(vec2 p) {  
   float a = .5;  
   float h = .0;  
   for (int i=0;i<10;i++) {  
       h += a*n2d(p);  
       a *= .5;  
       p *= 2.;  
   }  
   return h;  
}  
  
float ground(vec2 p) {  
   return (fbm(p*0.0015) - .5) * 1000. - 20.;  
}

此时效果如下:


分形噪声地形
看起来好多了,水面和水面法线也类似的办法处理一下:

float water(vec2 p) {  
   float w = n2d(p*0.1);  
   w += n2d(p*0.4) * .2;  
   w += n2d(p*1.5) * .04;  
   w += n2d(p*5.0) * .008;  
   return w;  
}  
  
vec3 calcWaterNormal(in vec3 pos) {  
   vec2 eps = vec2( 0.01, 0.0 );  
   return normalize( vec3(water(pos.xz-eps.xy) - water(pos.xz+eps.xy), 2.0*eps.x, water(pos.xz-eps.yx) - water(pos.xz+eps.yx)) );  
}

float raymarch(vec3 ro, vec3 rd, out int type) {  
   for (float t = 1.; t < 10000.;) {  
       vec3 pos = ro + t*rd;  
       float d = 0.001 * t;  
       float w = water(pos.zx);  
       if (pos.y - w < d) {  
           type = 0;  
           return t;  
       }  
  
       float y = ground(pos.xz);  
       if (pos.y - y <= d) {  
           type = 1;  
           return t;  
       }  
  
       t += 0.001+max((pos.y-max(w,y))*.5, d*2.);  
       //t += max(0.01, t * 0.01);  
   }  
   return 10000.; //未命中  
}

顺便在给天空调调色,加上一点渐变

col = mix(vec3(.6, .7, .9), vec3(.35, .62, 1.2), pow(max(rd.y + .15, 0.), .5));

效果如下:


分形地形和海面
六、高光、环境光与阴影
想让土质更加真实,需要对光照计算的更准确,还要加上阴影。
光追对于阴影的计算是比较擅长的,从碰撞点,向光源方向步进。如果撞山了,就是在阴影内,亮度系数为0,如果什么都没碰到就是有光照,亮度系数为1。
由于光线在物体的边缘会发生衍射,如果距离很远,且步进撞山的位置和山高非常接近,那么需要给亮度系数算个中间值。
代码如下:

float softShadow(in vec3 ro, float dis) {
    float start = clamp(dis*0.01,0.1,150.0);
    float res = 1.;
    vec2 tp;
    for( float t=start; t<3000.; t*=2. ) {
        vec3 pos = ro + t*LIGHT;
        float h = pos.y - ground(pos.xz);
        res = min( res, 16.0*h/t );
        if( res<0.001 || pos.y>2000. ) break;
    }
    return clamp( res, 0., 1.0 );
}

把画陆地的代码挪出到一个单独的函数,给陆地加个颜色,除了原来的单纯光照角度决定亮度,改为常用的直接光照+高光这种不准确但好用的模拟,再加上拍脑袋算的环境光。
其中直接光照和高光需要乘以前面的亮度系数。
高光是镜面反射,其反射强度可以用菲涅尔反射定律计算,设入射角度余弦值为fre,我们用Schlick近似公式:0.05+0.95*pow(fre,5.0) 来计算
由此得到画陆地的函数

vec3 drawMountain(vec3 pos, vec3 rd, vec3 lgt, float resT) {
    vec3 col = vec3(0.18,0.12,0.10)*.85; //给陆地加个颜色
    vec3 nor = calcNormal( pos, 0.0005*resT );

    float dif = clamp(dot( nor, lgt), 0., 1.);
    float ssh = softShadow(pos, resT);
    dif *= ssh; //阴影
    float bac = clamp(dot(normalize(vec3(-lgt.x,0.0,-lgt.z)),nor),0.,1.);
    vec3 lin = 5.5*vec3(1.0,0.9,0.8)*dif; //直接光照
    lin += 0.7*vec3(1.1,1.0,0.9)*bac; //随便模拟的背光
    col *= lin;

    vec3  ref = reflect(rd,nor);
    float fre = clamp(1.0+dot(nor,rd),0.,1.);
    float spc = clamp(dot(ref,lgt),0.,1.);
    float spe = 3.0*pow(spc, 5.0)*(0.05+0.95*pow(fre,5.0));
    col += spe*vec3(2.0)*ssh;
    return col;
}

把天空也单独挪到一个函数,给天空加一个太阳,视线和光线,点乘后加一个很小的值(太阳的圆面),然后上一个巨大的次方即可。

vec3 drawSky(vec3 rd) {
    vec3 c = mix(vec3(.6, .7, .9), vec3(.35, .62, 1.2), pow(max(rd.y + .15, 0.), .5)); //天空
    c += pow(max(dot(rd, LIGHT)+.0005, 0.), 3000.); //太阳
    return c;
}

效果如下


阴影七、光线追踪下的水面渲染
传统光栅的方法想把水画的真实非常难,但光追的方式就容易太多了。
由于不可能追踪所有光线,光线追踪都是捡主要的,如果某些光线和主要光线存在量级上的差距,就可以忽略它了。
这个是我自己想象的水渲染模型,算法不一定最优,仅供参考。
将光线分三部分计算:
1、 水面反射的光
这部分的比例和刚才高光的地方差不多,遵循菲涅尔反射定律,同样用5次方公式近似:0.02 + 0.98 * pow(1. - max(dot(-rd, normal), 0.), 5.)
它的反射光颜色就很简单了,把当前水面点当作眼睛所在处,反射光线的方向作为视线方向,重新来一次步进+画山画天空的操作就行

vec3 ref = normalize(reflect(rd, normal));
vec3 fCol;
int refType;
float refResT = raymarch(pos, ref, refType);
if (refResT < 10000.) {
    vec3 refPos = pos + refResT * ref;
    fCol = drawMountain(refPos, ref, LIGHT, refResT + resT);
} else {
    fCol = drawSky(ref);
}

2、 水底景物反射出来的光先算折射方向,然后沿折射方向朝水下步进,获得海底碰撞点像画陆地一样计算海底碰撞点的颜色,但需要注意的是,由于折射,水下的光源方向已经和水上有所不同,需要把折射后的方向传给drawMountain。光线到海底有衰减,我们假设每单位长度衰减a,水下步进距离是rT,那么到海底的光线强度应该是pow(1-a, rT) 光线从海底射出同样有衰减,从海底射出水面的距离可以用rT乘以两者光路在y轴上的比值,那么就是pow(1-a, -rT*ret.y/lightRet.y),ret.y和lightRet.y正负相反,所以需要加个负号 我们设a等于0.06,最终结果应该是

vec3 col = drawMountain(pis, ret, lightRet, resT + rT) * pow(0.94, rT-rT*ret.y/lightRet.y);

3、 水自身的颜色
水的颜色,由于水本身存在杂质,光在行进中会不断散射,散射光线往眼睛方向的加总
这里等我哪天画个示意图
视线到海底的光路上任意一点的散射光等于该点的光强*散射系数
设视线从水面沿着光路抵达该点的长度为x,那么光线折射后抵达该点的行进距离应该是-x*ret.y/lightRet.y,那么光强等于pow(0.94, -x*ret.y/lightRet.y)
假设散射系数为0.01,那么朝人眼的散射光就是0.01*pow(0.94, -x*ret.y/lightRet.y)
但这个光也是要经过x这么长的水域的,他也要衰减,在乘以pow(0.94, x),那么最终就是0.01*pow(0.94, x*(1-ret.y/lightRet.y))
然后对它做0到rT的积分
color=&#x2211;x=0rT0.01&#x2217;0.94x(1&#x2212;ret.y/lightRet.y)" role="presentation">color=∑x=0rT0.01?0.94x(1?ret.y/lightRet.y)color = \sum_{x=0}^{rT}0.01*0.94^{x(1-ret.y/lightRet.y)}
解得最终值为0.01 / k * (exp(rT*k) - 1.) 其中k = ln(0.94) * (1. - ret.y / lightRet.y)
然后drawSea的代码如下:

vec3 drawSea(vec3 pos, vec3 rd, float resT) {
    vec3 seaColor = vec3(0.25, 0.5, 0.75);
    vec3 normal = calcWaterNormal(pos, pow(resT*0.05, 2.7)*.0005);

    //折射
    vec3 ret = refract( rd, normal, 1.0f / 1.3333f );
    float rT = 0.01;
    vec3 pis;
    for(float a; rT < 1000.; rT += 0.01 + max(a*.5, (resT+rT)*.01)) {
        pis = pos + rT*ret;
        a = pis.y - ground(pis.xz);
        if (a <= .01*(resT+rT))
            break;
    }
    vec3 lightRet = -refract(-LIGHT, normal, 1.0f / 1.3333f);
    float m = (1. - ret.y / lightRet.y);
    float k = log(0.94) * m;
    seaColor *= 0.01 / k * (exp(rT*k) - 1.);
    if (rT < 1000.) {
        vec3 col = drawMountain(pis, ret, lightRet, resT + rT);
        seaColor += col * pow(0.94, rT*m);
    }
    seaColor *= 0.98 - 0.98 * pow(1. - max(dot(LIGHT, normal), 0.), 5.);

    //反射
    vec3 ref = normalize(reflect(rd, normal));
    //ref.y = abs(ref.y);
    vec3 fCol;
    int refType;
    float sun = 0.;
    float refResT = raymarch(pos, ref, refType);
    if (refResT < 10000.) {
        vec3 refPos = pos + refResT * ref;
        fCol = drawMountain(refPos, ref, LIGHT, refResT + resT);
    } else {
        fCol = drawSky(ref);
    }

    float fresnel = 0.02 + 0.98 * pow(1. - max(dot(-rd, normal), 0.), 5.);
    return mix(seaColor, fCol, fresnel);
}

执行一下,效果逐渐变得真实:


水面光影
可以注意到沿岸的阴影有点问题,这是因为水下的softShadow也有所不同,从海底碰撞点开始,先沿着折射光源方向步进,到了水面上以后,在按着正常光源方向步进,然后计算撞山点。
需要给softShadow加一个参数,阴影步进最大长度。
还需要给drawMountain多加两个参数,一个是传给softShadow的阴影步进最大长度,还有一个是已有阴影强度。然后手工计算已有阴影传给他。中间修改一下陆地的阴影计算dif *= min(waterSsh, ssh)
另外drawSea的画海底部分也要改一下:

        float ssh = clamp(softShadow(pos, 10000., resT));
        vec3 col = drawMountain(pis, ret, lightRet, resT + rT, rT, ssh);
        seaColor += col * pow(0.94, rT*m);

未完待续....改天再写
写的好累,离放在shadertoy的那个成品还有不小距离,只是后面越来越抽象,描述清楚越来越困难。
有些老代码还没保留,临时重新手写当初的版本,不一定对。不少地方的代码写了有一段时间了,临时重现思路可能有点偏差,回头再检查检查。
有能力的自己先看例子的完整代码吧,不知道要歇几天才会想起来接着写,先列个剩余章节的目录。
八、体积云九、加入时间的动态画面十、太阳角度与大气散射十一、云层优化与阳光散射十二、山体优化(模型算法、植被、雪、潮湿反光)
分享一些流体技巧:
场景大小对流体的制作方法影响很大,一般小规模的水流或者水滴是把粒子转变成poly,其他答案已经提到了。规模较大的流体(比如瀑布,喷溅,有互动的海水)一般是许多元素混合出的效果,以下几种方法可能混合使用(软件Houdini) :
- particle convert to poly(particle fluid surface) : 有可能会基于vel或者density控制转换后的形状
- particle to vdb to poly (vdb from particle fluid) : 不同方法但原理一样
- vex控制,根据密度拆分,低密度散溅的粒子copy stamp sphere,大密度的转换成poly或者vdb同上两种
然后流体本身的render pass:
- render as liquid: 以上结果正常方法渲染
- render as volume: 以上结果按volume渲染
- render as mask: 根据vel生成黑白遮罩,合成时根据这个来把水的volume合进去,给水增加体积感
-以及额外可能产生的各种pass
基于particle的结算进一步加入white water:
- particle render as volume(foam、bubble、spray): 粒子做volume渲染增加泡沫、喷溅细节
- particle render as partile: 粒子直接做粒子渲染,进一步增加流体细节
继续加入水雾和烟雾(mist/smoke)
以及每个element设定各种AOV给合成用。
另:以上一切基于好灯光、合成和给力的机器。
不足之处欢迎大家指正,谢谢。
最近正在做fluid fx 强答一发(*′?v?)
houdini里面一般可以转化为levelsets然后smooth后转化为polygon进行渲染
如果是splash,瀑布或者whitewater等可以用wisps进行volumetric rendering
既然题主主知道流体效果大多数是由粒子模拟出来。那不知道题主是否知道,粒子模拟后的下一步呢?
下一步就是根据粒子的密度,分布范围,来组成封闭模型,
划重点!!!!
这个模型就是拿去渲染的水模型了。
至于题主说的 高逼格
我不知道是否牵扯到气泡,白色水沫,以及运动模糊
不过对于流体模拟这一块houdini真的极有代表性,题主不妨去学习学习。
至于教程,sidefx的官网有教程,有题主想要的流体模拟以及渲染的教程。
前言
我们已经了解到了在静态时,一个物体或者场景是如何开始渲染的。但是不论是在影视还是游戏作品中,内容都是动态的,本篇内容我们就学习在动画时是如果计算内容并且显示到屏幕上的
动画第一部分
动画在图形学里可以看作是对几何的拓展。形成是用多张图连续播放,不同设备有不同的帧率要求。90Hz的帧率正在成为VR头显的黄金标准,这有助于缓解在较低帧率下可能发生的晕动症和头痛。


第一部手绘的剧场版动画


两个主要动作的图片是关键帧,中间补出的过程是中间帧


在每个关键帧里找一些关键点,再使用插值的方法将中间帧的这些点的位置算出来


插值的方法


线性插值和样条线物理模拟的运作方式


衣服的每个点都会受到重力等影响,我们需要计算大部分点的物理模拟就可以得到运动情况,建立一个受力模型


还有流体的物理模拟


质点弹簧系统(MassSpringSystem)
质点弹簧系统是以一个类似可以弹动的绳子为研究方向的一个内容


质点弹簧模型(MassSpringMesh)
将弹簧系统延申一下就可以做出弹簧模型


模拟后的效果是非常的好的


如下A点和B点相连中间弹簧的长度忽略,互相之间有一个作用力,Ks是劲度系数


这里的l代表的是弹簧正常的时候的长度,因为能量守恒弹簧的运动永远不会停


所以我们需要添加一个摩擦力
这里模拟仿真用如下的方式表示速度和加速度


想让弹簧停下来需要添加一个与速度方向相反的摩擦力


我们需要添加的不是外部的里而是内部的相对的力,不然只会导致弹簧横向移动


总结
最近工作有点忙本篇笔记内容先总结到这里,可能会分两到三部分写图型学动画的学习笔记,感谢大家的阅读!


核心:数学公式
[收藏本文] 【下载本文】
   明星艺术 最新文章
为什么我欣赏不了启功先生的字?
为什么我国少数民族语言几乎只有一种字体?
为什么2025年日本大阪世博会场馆建设没有人
为什么许多人认为wlop画的鬼刀并不好?
如何评价画师影法师?
怎样评价徐阶?
你见过哪些非常搞笑的体育摄影照片?
为什么中国人对玉石情有独钟,而欧美人兴趣
漫画中「画风」和「画崩」的区别是什么?
传统相声《揭瓦》的难点主要体现在何处?
上一篇文章           查看所有文章
加:2025-03-17 14:12:06  更:2025-03-17 14:16:34 
 
古典名著 名著精选 外国名著 儿童童话 武侠小说 名人传记 学习励志 诗词散文 经典故事 其它杂谈
小说文学 恐怖推理 感情生活 瓶邪 原创小说 小说 故事 鬼故事 微小说 文学 耽美 师生 内向 成功 潇湘溪苑
旧巷笙歌 花千骨 剑来 万相之王 深空彼岸 浅浅寂寞 yy小说吧 穿越小说 校园小说 武侠小说 言情小说 玄幻小说 经典语录 三国演义 西游记 红楼梦 水浒传 古诗 易经 后宫 鼠猫 美文 坏蛋 对联 读后感 文字吧 武动乾坤 遮天 凡人修仙传 吞噬星空 盗墓笔记 斗破苍穹 绝世唐门 龙王传说 诛仙 庶女有毒 哈利波特 雪中悍刀行 知否知否应是绿肥红瘦 极品家丁 龙族 玄界之门 莽荒纪 全职高手 心理罪 校花的贴身高手 美人为馅 三体 我欲封天 少年王
旧巷笙歌 花千骨 剑来 万相之王 深空彼岸 天阿降临 重生唐三 最强狂兵 邻家天使大人把我变成废人这事 顶级弃少 大奉打更人 剑道第一仙 一剑独尊 剑仙在此 渡劫之王 第九特区 不败战神 星门 圣墟
  网站联系: qq:121756557 email:121756557@qq.com