博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
水波特效处理
阅读量:5250 次
发布时间:2019-06-14

本文共 10745 字,大约阅读时间需要 35 分钟。

这篇博文译自以下这篇文章——

由于这篇文章主要用Pascal语言进行描述的。因此我后面会添加一些注释,并结合Apple提供的ripple相关的Demo给出一些额外的遵守GNU11规范的C代码。

介绍

在计算机图形中的许多特效中,水特效是一种完全抓取观众注意的效果。它模拟了水在被外界干扰时的行为。

这篇文章由两部分组成。第一部分介绍了水的行为如何被模拟。第二部分描述了当光照射到透明的表面时,你可以如何计算光的折射。它们一起为你提供了对一个抓取视线模拟程序的知识。

第1部分-水波如何被模拟

隐藏在这种特效后的机制非常简单。它太简单了,以至于我相信它是在对区域采样的实验中偶然被发明的。但在我深入水波模拟背后的计算之前,我将告诉你一些关于区域采样的知识。

区域采样

区域采样在计算机图形学中是一种非常普遍的算法。考虑一个二维图,在(x, y)处的值受(x, y)位置的周围值的影响,诸如(x+1, y),(x-1, y),(x, y+1),以及(x, y-1)。我们的水波模拟实际上在三个维度上工作,但我们将在后面谈到这点。

区域采样例子:一个简单的模糊

将一个图进行模糊非常简单。你将需要两个图:一个含有你想要模糊的数据,一个用于生成结果图。算法(使用五个样本值)看上去像以下形式:

ResultMap[x, y] := (SourceMap[x, y] +    SourceMap[x+1, y] +    SourceMap[x-1, y] +    SourceMap[x, y+1] +    SourceMap[x, y-1]) DIV 5;

用直白的话来说,(x, y)的值依赖于周围值的平均值。[译者注:这边的值是指像素值,或像素各个分量到值,(x, y)的像素值由其周围5个点的像素值的算术平均数计算得到。]当然,当你想要模糊图像时事情会变得有一点复杂,不过你获得了这种想法。

创建一个水波模拟基本上是相同的,但是(x, y)处的值以不同的方式计算。之前我提到我们的水波模拟以三个维度进行工作。好吧,我们的第三个维度就是时间。换句话说,在计算我们的水波模拟时,我们必须知道水波在此前一刻看上去像啥。结果图在下一帧中变为源图。

这是实际的水波模拟算法:

ResultMap[x, y] := ((CurrentSourceMap[x+1, y] +    CurrentSourceMap[x-1, y] +    CurrentSourceMap[x, y+1] +    CurrentSourceMap[x, y-1]) DIV 2) - PreviousResultMap[x, y]

你将注意到首先从当前源图中所获得的四个值被2除。结果产生了两倍的均值。然后,我们将这个值减去在先前结果图中的工作位置(x, y)的值。这产生了一个新值。看图a和图b来获悉这如何影响水波。

水平灰线表示水波的平均高度[译者注:这条线作为考察水波高度走势的基准线,而不是x轴。水平方向可以看作为位置,垂直方向为水波高度。水平方向各个点随时间变化上下起伏。]。如果在(x, y)的先前值比平均值要小,那么水波将向上升到平均水平,正如图a所示的那样。

如果在(x, y)处的先前的值比平均值高,那么正如图b所示的那样,水波将下降到平均水平。

阻尼

一个水波每次上下移动时,其能量会分布在一个扩展区域上。这意味着水波的振幅一直下降直到水波达到平衡[译者注:即水面恢复平静]。我们可以使用一个阻尼系数来模拟这种情况。该因子,振幅的某个百分量,从当前的振幅减去以让高振幅快速消失,并且低振幅缓慢消失。在以下例子中,当每次水波移动时,振幅的十六分之一被减去。

水波模拟例子

下列代码片段一开始包含了某个内联汇编器,但我用本地的Pascal代码代替它了,这样它可以更容易地被移植到任一语言以及任一平台。

const    MAXX = 320;    { 水波图的宽度和高度 }    MAXY = 240;    DAMP = 16;      { 阻尼系数 }{ 定义水波图WaveMap[frame, x, y]以及帧索引 }var    WaveMap: Array[0..1, 0..(MAXX - 1), 0..(MAXY-1)] of SmallInt;    CT, NW: SmallInt;procedure UpdateWaveMap;var    x, y, n: SmallInt;begin    { 跳过边界以允许区域采样 }    for y := 1 to MAXY - 1 do begin        for x := 1 to MAXX - 1 do begin            n := (WaveMap[CT, x-1, y] + WaveMap[CT, x+1, y] +            WaveMap[CT, x, y-1] + WaveMap[CT, x, y+1]) div 2 -             WaveMap[NW, x, y];            n := n - (n div DAMP);            WaveMap[NW, x, y] := n;        end;    end;end;

当这代码被执行时,你要将结果绘制到一个图像缓存。这如何实现在第2部分中解释。重要的是你在绘制图像之后要为下一次迭代交换源和结果图:

Temporary_Value := CT;CT := NW;NW := Temporary_Value;

不过CT和NW意思是什么呢?CT和NW是指向不同水波图的变量。CT是当前水波图,它含有我们需要生成新的水波图的数据,被NW所指。CT和NW可以持有两个值,0和1,并且可以一直不能相同。因为我们在每次迭代后交换这两个图,新的水波图含有在当前水波图之前所生成的水波图的数据。我意识到这可能听上去复杂,但这并不是那样。

使它移动

上述过程简单地让水波平静下来。那么,我们如何能让整个水波移动呢?确切地说,是通过削减水波位图中的值。一个未受外界干扰的水波图仅包含零值。要创建一个水波,只要挑选一个随即位置并改变这个值,就像下面那样:

WaveMap[x, y] := -100;

值越大,水波越大。

第2部分——透明表面光照追踪

现在,我们有自己的水波图,我们想对它玩一些把戏。我们取一束光,让它垂直地照射穿过水表面。因为水比空气具有更高的密度,所以光线向表面发现进行折射,并且我们可以计算光束照射到哪儿,不管那底下是啥(比如一个图像)。

首先,我们需要知道在入射光与表面法线之间的角度是啥(图c)。

在图c中,红线表示表面法线。穿过水波图的垂直线表示入射光,而连接垂线的箭头是折射光线。正如你所能看见的那样,在折射光与表面法线之间的角度比入射光与表面法线之间的角度要小。

确定入射光的角度

这通过测量在(x, y)与(x-1, y)之间以及(x, y)和(x, y-1)的高度差来实现。这给了我们单位为1的三角形。角度为arctan(高度差 / 1),或arctan(高度差)。看图d来进行解释:

计算表面法线与入射光之间的角度在我们的实例中非常简单。如果我们画一个假象的三角形,这里用红色表示,那么我们需要做的就是确定alpha。当我们用x(为1)去除y(为高度差)时,我们就得到了alpha的正切。换句话说,高度差是alpha的高度差,并且alpha是ArcTan(高度差)。

为了要为你鉴证这个事实——这个实际上是表面法线与入射光之间的角度——我将红色三角形按逆时针旋转90度。正如你所看到的,斜边与表面法线平行。[译者注:这里其实也采用了微分方法。图d中斜边为图c中的正弦曲线上的一小段,水平方向取1个单位,相应获得水波图中两个相邻位置的水波高度差,即为图d中的直角边。这就非常容易证明入射光与法线的夹角与alpha是相等的——含有一个公共角的两个直角的邻角相等。]

下一步,我们计算折射角。如果你记得大学里的物理,那么你知道:

折射率 = sin(入射光的角度) / sin(折射光的角度)

这样,被折射光线的角度可以这么被计算出:

折射光的角度 = arcsin(sin(入射光的角度) / 折射率)

这里,折射率是水的折射率:2.0。

第三,我们需要计算折射光照射到图像哪里,或者它与入射光原始进入的地方的相对位置:

位移 = tan(折射光的角度) * 高度差

透明表面的光线追踪的例子

下列代码片段没有被优化,因为这样,你不会错过计算上的很多细节。

for y:= 1 to MAXY-1 do begin    for x := 1 to MAXX-1 do begin        xDiff := Trunc(WaveMap[x+1, y] - WaveMap[x, y]);        yDiff := Trunc(WaveMap[x, y+1] - WaveMap[x, y]);        xAngle := arctan(xDiff);        xRefraction = arcsin(sin(xAngle) / rIndex);        xDisplace := Trunc(tan(xRefraction) * xDiff);        yAngle := arctan(yDiff);        yRefraction := arcsin(sin(yAngle) / rIndex);        yDisplace := Trunc(tan(yRefraction) * yDiff);        if xDiff < 0 then begin            { 当前位置为更高值 - 顺时针方向旋转 }            if yDiff < 0 then                newColor := BackgroundImage[x-xDisplace, y-yDisplace]            else                newColor := BackgroundImage[x+xDisplace, y+yDisplace]        end;        TargetImage[x, y] := newColor;    end;end;

 

以下是Apple提供的一个水波纹理的Demo,里面有我写的一些注释,应该已经比较清除了,呵呵:

/*     File: RippleModel.m Abstract: Ripple model class that simulates the ripple effect.  Version: 1.0   Disclaimer: IMPORTANT:  This Apple software is supplied to you by Apple Inc. ("Apple") in consideration of your agreement to the following terms, and your use, installation, modification or redistribution of this Apple software constitutes acceptance of these terms.  If you do not agree with these terms, please do not use, install, modify or redistribute this Apple software.   In consideration of your agreement to abide by the following terms, and subject to these terms, Apple grants you a personal, non-exclusive license, under Apple's copyrights in this original Apple software (the "Apple Software"), to use, reproduce, modify and redistribute the Apple Software, with or without modifications, in source and/or binary forms; provided that if you redistribute the Apple Software in its entirety and without modifications, you must retain this notice and the following text and disclaimers in all such redistributions of the Apple Software. Neither the name, trademarks, service marks or logos of Apple Inc. may be used to endorse or promote products derived from the Apple Software without specific prior written permission from Apple.  Except as expressly stated in this notice, no other rights or licenses, express or implied, are granted by Apple herein, including but not limited to any patent rights that may be infringed by your derivative works or by other works in which the Apple Software may be incorporated.   The Apple Software is provided by Apple on an "AS IS" basis.  APPLE MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS.   IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION, MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE), STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.   Copyright (C) 2013 Apple Inc. All Rights Reserved.   */ #import "RippleModel.h" @interface RippleModel () {    unsigned int screenWidth;    unsigned int screenHeight;    unsigned int poolWidth;     // 水平方向所要绘制的网格数    unsigned int poolHeight;    // 垂直方向所要绘制的网格数    unsigned int touchRadius;   // 手指触摸屏幕后初始的水波半径         unsigned int meshFactor;    // 网格宽度(iPhone上默认设置为4;iPad上默认设置为8)         float texCoordFactorS;      // 用于将纹理坐标规格化的水平方向上的单位宽度    float texCoordOffsetS;      // 纹理水平方向的偏移;此偏移由于可能要针对高度做规格化而产生的位置偏差    float texCoordFactorT;      // 用于将纹理坐标规格化的垂直方向上的单位高度    float texCoordOffsetT;      // 纹理垂直方向的偏移;此偏移由于可能要针对宽度做规格化而产生的位置偏差         // ripple coefficients    float *rippleCoeff;         // 水波系数表,实际长度为float[2*touchRadius+1][2*touchRadius+1]         // ripple simulation buffers    float *rippleSource;        // 源水波    float *rippleDest;          // 目的水波         // data passed to GL    GLfloat *rippleVertices;    // 水波顶点坐标;每个元素为struct {float x, y;};类型    GLfloat *rippleTexCoords;   // 水波纹理坐标;每个元素为struct {float s, t;};类型    GLushort *rippleIndicies;   } @end @implementation RippleModel - (void)initRippleMap{    // +2 for padding the border    memset(rippleSource, 0, (poolWidth+2)*(poolHeight+2)*sizeof(float));    memset(rippleDest, 0, (poolWidth+2)*(poolHeight+2)*sizeof(float));} // 在以(2 * touchRadius + 1)为边长的正方形的内切圆内计算各个像素点所对应的水波振幅系数- (void)initRippleCoeff{    // 一共(2 * touchRadius + 1)行    for (int y=0; y <= 2*touchRadius; y++)    {        // 每行有(2 * touchRadius + 1)个点        for (int x=0; x <= 2*touchRadius; x++)        {            // 当前点到圆心(touchRadius, touchRadius)的距离。            // 若当前点正好在圆心上,则distance为0。            float distance = sqrt((x-touchRadius)*(x-touchRadius)+(y-touchRadius)*(y-touchRadius));                         if (distance <= touchRadius)            {                // 若当前点在内切圆的范围内,则计算该点的系数。                float factor = distance / touchRadius;  // 该因子的取值范围是[0, 1]                 // goes from -512 -> 0                // 赋值给当前点的系数。系数的确定是通过由中心点(touchRadius, touchRadius)作为起始点,在正方形内切圆范围内作cos波形扩散。                // 使用余弦是因为它是偶函数,正好与y轴(这里表示水波的振幅)对称。这里的余弦函数的取值范围是[-1, 1],并且正好是半个周期,由于distance的范围是[0, 1]。                // 这里可以看到使用-cos(factor * π)因为在起始点处(也就是手指点下去的那一点),初始波的振幅是向下(负方向)绝对值最大的。                // 然后获得的振幅加1,再乘以256,使得最终值定格在[-512, 0],用于量化。                rippleCoeff[y*(touchRadius*2+1)+x] = -(cos(factor*M_PI)+1.f) * 256.f;            }            else            {                // 内切圆边界外的系数设为0                rippleCoeff[y*(touchRadius*2+1)+x] = 0.f;            }        }    }   } // 初始化网格- (void)initMesh{    // 先针对网格初始化顶点坐标以及纹理坐标    for (int i=0; i
< -0.5f) ? -0.5f : s_offset; t_offset = (t_offset < -0.5f) ? -0.5f : t_offset; s_offset = (s_offset > 0.5f) ? 0.5f : s_offset; t_offset = (t_offset > 0.5f) ? 0.5f : t_offset; // 获取当前正常的纹理坐标 float s_tc = (float)y/(poolHeight-1) * texCoordFactorS + texCoordOffsetS; float t_tc = (1.f - (float)x/(poolWidth-1)) * texCoordFactorT + texCoordOffsetT; // 真正获取所要采样的纹理坐标 rippleTexCoords[(y*poolWidth+x)*2+0] = s_tc + s_offset; rippleTexCoords[(y*poolWidth+x)*2+1] = t_tc + t_offset; } }); // 这一步用来交换源水波与目的水波,使得当前的目的水波将作为后一帧的源水波 float *pTmp = rippleDest; rippleDest = rippleSource; rippleSource = pTmp; } // 在手指点的位置处设置rippleSource- (void)initiateRippleAtLocation:(CGPoint)location{ // 当前位置所对应的网格索引 unsigned int xIndex = (unsigned int)((location.x / screenWidth) * poolWidth); unsigned int yIndex = (unsigned int)((location.y / screenHeight) * poolHeight); // 以当前位置为圆心,touchRadius为半径,根据水波系数设置水波源 for (int y=(int)yIndex-(int)touchRadius; y<=(int)yIndex+(int)touchRadius; y++) { for (int x=(int)xIndex-(int)touchRadius; x<=(int)xIndex+(int)touchRadius; x++) { // 仅对在网格区域范围内的水波系数和水波源进行操作 if (x>=0 && x
=0 && y

转载于:https://www.cnblogs.com/SarielTang/p/4501440.html

你可能感兴趣的文章
C# 强制关闭当前程序进程(完全Kill掉不留痕迹)
查看>>
ssm框架之将数据库的数据导入导出为excel文件
查看>>
语音识别中的MFCC的提取原理和MATLAB实现
查看>>
使用AVCaptureSession捕捉静态图片
查看>>
bugku web 头等舱
查看>>
Convert.ToInt32、int.Parse(Int32.Parse)、int.TryParse三者之间的区别
查看>>
算法之【仿竖式算法】
查看>>
java string
查看>>
验证组件FluentValidation的使用示例
查看>>
0320-学习进度条
查看>>
JAVA跨域CORS
查看>>
正确的在循环list的时候删除list里面的元素
查看>>
ERP渠道文档详细和修改(二十五)
查看>>
C#正则Groups高级使用方法
查看>>
ecshop安装常见问题及解决办法
查看>>
解决windows系统的oracle数据库不能启动ora-00119和ora-00130的问题
查看>>
ip相关问题解答
查看>>
第九周作业
查看>>
Postman—添加断言和检查点
查看>>
网络文件下载
查看>>