针对这两篇教程:

http://www.keithlantz.net/2011/10/ocean-simulation-part-one-using-the-discrete-fourier-transform/

http://www.keithlantz.net/2011/11/ocean-simulation-part-two-using-the-fast-fourier-transform/

的一些注解:

一,

原文下面一段

框起部分不通顺,似乎应该是 "by letting Lx=N and Lz=M. then becomes,"

从这段之后教程中本应是Lx,Lz的地方就都变成M,N了。

由此看来,作者为了方便使用fft计算heightmap,强制使Lx,Lz等于M,N了。

而在教程的非fft的版本中是没有Lx,Lz必须等于N,M的限制的,甚至Lx,Lz可以不是整数。

即对于非fft版本,ocean的构造函数

cOcean::cOcean(const int N, const float A, const vector2 w, const float length, const bool geometry)

中N和length可以传不同的值,但对于fft版本,N和length必须传相同整数值才正确(N须为2的幂)。

补充:坑爹,原来我用的浏览器有问题,导致教程中插在句子中的数学符号显示不出来,刚才用另一个浏览器看,发现红框中那句确实是:

整篇教程中类似情况不胜枚举,我居然一直都是在缺符号的情况下看的。。。

二,

教程中h_tilde为NxN,vertices为(N+1)x(N+1)。

vertices的初始化代码为:

for (int m_prime = 0; m_prime < Nplus1; m_prime++) {
        for (int n_prime = 0; n_prime < Nplus1; n_prime++) {
            index = m_prime * Nplus1 + n_prime;
            htilde0        = hTilde_0( n_prime,  m_prime);
            ...
            vertices[index].ox = vertices[index].x =  (n_prime - N / 2.0f) * length / N;
            vertices[index].oy = vertices[index].y =  0.0f;
            vertices[index].oz = vertices[index].z =  (m_prime - N / 2.0f) * length / N;
    ...
        }
    }

从中可以看出h'(x,z,t)的x和z的初始表达式为:

x=(n'-N/2)*L/N

z=(m'-N/2)*L/N

对于fft版本,由于教程中强制令L=N,所以

x=n'-N/2

z=m'-N/2

(注,后面为了实现“卷浪”,x,z在此基础上还会进行一定的偏移)。

三,

教程中fft版本的总体计算思路是将二维DFT:

拆成两个一维DFT来进行计算

以N=4为例,计算过程如下:

其中DFT(4)代表(4)式所定义的DFT,DFT(3)代表(3)式所定义的DFT。

即先逐行dft,再逐列dft。

可见对于N=4的情况,完成heightmap的更新需要计算8个DFT,那么对于一般情况则需要计算2N个DFT。

每个DFT都可以使用FFT来计算,对应教程中如下代码:

for (int m_prime = 0; m_prime < N; m_prime++) {
        fft->fft(h_tilde, h_tilde, 1, m_prime * N);
        …
    }
    for (int n_prime = 0; n_prime < N; n_prime++) {
        fft->fft(h_tilde, h_tilde, N, n_prime);
        …
    }

四,

教程代码中实现无缝循环(tiling)的代码非常简单,只是将第0行(列)的数据填充给第N+1行(列)即可:
    for (int m_prime = 0; m_prime < N; m_prime++) {
        for (int n_prime = 0; n_prime < N; n_prime++) {
            index  = m_prime * N + n_prime;        // index into h_tilde..
            index1 = m_prime * Nplus1 + n_prime;    // index into vertices
            …
            // height
            vertices[index1].y = h_tilde[index].a;
            …
            // for tiling
            if (n_prime == 0 && m_prime == 0) {
                vertices[index1 + N + Nplus1 * N].y = h_tilde[index].a;
                …
            }
            if (n_prime == 0) {
                vertices[index1 + N].y = h_tilde[index].a;
                …
            }
            if (m_prime == 0) {
                vertices[index1 + Nplus1 * N].y = h_tilde[index].a;
                …
            }
        }
    }

言外之意,h'(x,z,t)在x和z方向上均是以L为周期的周期函数,即:

h'(x+L,z,t)=h(x,z,t)

h'(x,z+L,t)=h(x,z,t)

事实确实如此,验证如下:

因为N为偶数,所以

所以

h'(x+L,z,t)=h(x,z,t)

h'(x,z+L,t)=h(x,z,t)

五,

evaluateWavesFFT函数中如下代码:

for (int m_prime = 0; m_prime < N; m_prime++) {
        fft->fft(h_tilde, h_tilde, 1, m_prime * N);
        …
    }
    for (int n_prime = 0; n_prime < N; n_prime++) {
        fft->fft(h_tilde, h_tilde, N, n_prime);
        …
    }
    int sign;
    float signs[] = { 1.0f, -1.0f };
    vector3 n;
    for (int m_prime = 0; m_prime < N; m_prime++) {
        for (int n_prime = 0; n_prime < N; n_prime++) {
            index  = m_prime * N + n_prime;        // index into h_tilde..
            index1 = m_prime * Nplus1 + n_prime;    // index into vertices
            sign = signs[(n_prime + m_prime) & 1];//如果n+m为奇数,则sign=signs[1]=-1;如果n+m为偶数,则sign=signs[0]=1
            h_tilde[index]     = h_tilde[index] * sign;
            …
        }
    }

可见在计算完2N次FFT后,还有一个符号校正(乘以sign)的步骤。

为什么需要符号校正?

因为代码中的第一个fft并不是完整计算了h''(x,m',t),而只是计算了下式中红框内的部分(省略了一个(-1)^x因子):

同理,代码中的第二个fft也并不是完整计算了h'(x,z,t),而只是计算了下式中红框内的部分(省略了一个(-1)^z因子):

所以当fft计算全部执行完后,还需要乘以(-1)^(x+z)。

因为在fft版本中x=n'-N/2,z=m'-N/2,所以(-1)^(x+z)=(-1)^(n'+m'-N),因为N为偶数,所以(-1)^(n'+m'-N)=(-1)^(n'+m')。

六,

蝶形图及T函数表实现

这一块发现教程中有些错误,我们自己来推导一下N=4的蝶形图。

在上式中令N=4,得:

因x=n'-N/2,n'=0,1,2,3,所以x=-2,-1,0,1,并适当利用变形,得:

据此可画出蝶形图:

对上面蝶形图进行等价优化变形,得:

注:以上优化变形的原理是:因为,所以有T4^0=-T4^(-2),故可将T4^(-2)由支干移至主干,并将T4^0改为-1。同理,可将T4^(-1)由支干移至主干,并将T4^1改为-1。

容易看出变换后的蝶形图与原蝶形图等价。此优化思路源于:https://cnx.org/contents/zmcmahhR@7/Decimation-in-time-DIT-Radix-2,其中的Additional Simplification一节。

此即为N=4时的最终蝶形图,可以验证此蝶形图与教程中给出的N=4蝶形图是不等价的,即教程中蝶形图有误。

再看教程中生成T函数表的代码:

cFFT::cFFT(unsigned int N) : N(N), reversed(0), T(0), pi2(2 * M_PI) {
    …
    int pow2 = 1;
    T = new complex*[log_2_N];        // prep T
    for (int i = 0; i < log_2_N; i++) {
        T[i] = new complex[pow2];
        for (int j = 0; j < pow2; j++) T[i][j] = t(j, pow2 * 2);
        pow2 *= 2;
    }
    …
}
complex cFFT::t(unsigned int x, unsigned int N) {
    return complex(cos(pi2 * x / N), sin(pi2 * x / N));
}

显然也是有错误的,其中

for (int j = 0; j < pow2; j++) T[i][j] = t(j, pow2 * 2);

一句,应改为:

for (int j = 0; j < pow2; j++) T[i][j] = t(j-N/2, pow2 * 2);

因为x=n'-N/2,而非x=n'。

此至,教程及代码中的疑问基本上就都解决了,下面是移植到unity中的结果:

(为了确认无缝,在z轴方向平铺了一次)

----补充:

一,

蝶形图的含义:

首先我们知道蝶形图右边任何一个输出都是左边所有输入的线性组合,所以假设我们想知道h'''(-1,m',t)的表达式,首先有

然后再根据蝶形图读出A,B,C,D的值。

求A的值,看红色路径,没有经过任何值,所以A=1。

求B的值,看绿色路径,经过T2^(-2)和-1,所以B=-T2^(-2)。

求C的值,看蓝色路径,经过T4^(-1),所以C=T4^(-1)。

求D的值,看黄色路径,经过T2^(-2),-1,T4^(-1),所以D=-T2^(-2)*T4^(-1)。

所以得:

对比前面(六)中所得:

因为-T2^(-2)=T2^(-1),所以结果是一样的。

二,

比特翻转

根据蝶形图实现fft,因为蝶形图左边N个输入数据的是乱序(比如对于N=4而言是0,2,1,3;对于N=8而言是0,4,2,6,1,5,3,7),这个次序可由bit reverse来生成,以N=4为例:

0 (00)   -- bit reverse --> (00) 0

1 (01)   -- bit reverse --> (10) 2

2 (10)   -- bit reverse --> (01) 1

3 (11)   -- bit reverse --> (11) 3

参考:http://www.dspguide.com/ch12/2.htm

代码中实现bit reverse的部分为:

cFFT::cFFT(unsigned int N) : N(N), reversed(0), T(0), pi2(2 * M_PI) {
    …
    log_2_N = log(N)/log(2);

reversed = new unsigned int[N];        // prep bit reversals
    for (int i = 0; i < N; i++) reversed[i] = reverse(i);
    …
}
unsigned int cFFT::reverse(unsigned int i) {
    unsigned int res = 0;
    for (int j = 0; j < log_2_N; j++) {
        res = (res << 1) + (i & 1);
        i >>= 1;
    }
    return res;
}

----补充2

此教程为cpu实现fft ocean,且fft部分的实现方法用的是最直观的方法,所以效率是比较不高的方法。

但即使cpu fft ocean再怎么优化,效果肯定也高不到哪儿去,所以此教程仅作为一个起点。接下来移植到gpu是必须的。等移植完我再另写一篇日志。

2017-6-25更新:

基于gpu的实现已初步试验成功,见下帖中的2017-6-25:http://www.cnblogs.com/wantnon/p/6985141.html

2017-6-30更新:

gpu fft海面动画已实现,见下帖中的2017-6-30: http://www.cnblogs.com/wantnon/p/6985141.html

----

另附两个重要参考:

http://graphics.ucsd.edu/courses/rendering/2005/jdewall/tessendorf.pdf

https://pdfs.semanticscholar.org/0047/8af7044a7f1350d5ec75ffc7c15b40057051.pdf

fft ocean注解的更多相关文章

  1. NV SDK 9.5, 10 and 11

    NVIDIA SDK 10   Overview This all-new collection of DirectX 10 and OpenGL code samples teaches devel ...

  2. 基于spring注解AOP的异常处理

    一.前言 项目刚刚开发的时候,并没有做好充足的准备.开发到一定程度的时候才会想到还有一些问题没有解决.就比如今天我要说的一个问题:异常的处理.写程序的时候一般都会通过try...catch...fin ...

  3. [Spring]IoC容器之进击的注解

    先啰嗦两句: 第一次在博客园使用markdown编辑,感觉渲染样式差强人意,还是github的样式比较顺眼. 概述 Spring2.5 引入了注解. 于是,一个问题产生了:使用注解方式注入 JavaB ...

  4. Android注解使用之通过annotationProcessor注解生成代码实现自己的ButterKnife框架

    前言: Annotation注解在Android的开发中的使用越来越普遍,例如EventBus.ButterKnife.Dagger2等,之前使用注解的时候需要利用反射机制势必影响到运行效率及性能,直 ...

  5. Android注解使用之注解编译android-apt如何切换到annotationProcessor

    前言: 自从EventBus 3.x发布之后其通过注解预编译的方式解决了之前通过反射机制所引起的性能效率问题,其中注解预编译所采用的的就是android-apt的方式,不过最近Apt工具的作者宣布了不 ...

  6. spring注解源码分析--how does autowired works?

    1. 背景 注解可以减少代码的开发量,spring提供了丰富的注解功能.我们可能会被问到,spring的注解到底是什么触发的呢?今天以spring最常使用的一个注解autowired来跟踪代码,进行d ...

  7. 编写高质量代码:改善Java程序的151个建议(第6章:枚举和注解___建议88~92)

    建议88:用枚举实现工厂方法模式更简洁 工厂方法模式(Factory Method Pattern)是" 创建对象的接口,让子类决定实例化哪一个类,并使一个类的实例化延迟到其它子类" ...

  8. ASP.NET MVC5----常见的数据注解和验证

    只要一直走,慢点又何妨. 在使用MVC模式进行开发时,数据注解是经常使用的(模型之上操作),下面是我看书整理的一些常见的用法. 什么是验证,数据注解 验证 从全局来看,发现逻辑仅是整个验证的很小的一部 ...

  9. 基于注解的bean配置

    基于注解的bean配置,主要是进行applicationContext.xml配置.DAO层类注解.Service层类注解. 1.在applicationContext.xml文件中配置信息如下 &l ...

随机推荐

  1. Asp.Net Core 2.0 项目实战(1) NCMVC开源下载了

    Asp.Net Core 2.0 项目实战(1) NCMVC开源下载了 Asp.Net Core 2.0 项目实战(2)NCMVC一个基于Net Core2.0搭建的角色权限管理开发框架 Asp.Ne ...

  2. How to cast List<Object> to List<MyClass> Object集合转换成实体集合

    List<Object> list = getList(); return (List<Customer>) list; Compiler says: cannot cast  ...

  3. 类 __new__方法实现单例

    继承了单例的类,子类也是单例模式

  4. HDU 2222 Keywords Search (AC自动机)(模板题)

    <题目链接> 题目大意: 给你一些单词,和一个字符串,问你这个字符串中含有多少个上面的单词. 解题分析: 这是多模匹配问题,如果用KMP的话,对每一个单词,都跑一遍KMP,那么当单词数量非 ...

  5. P2399 non hates math

    P2399 non hates math将分数化成小数的模拟题,把循环减掉就可以了.1.1(234)*10^4==11234.234*10^1==11.2349999*(1.1(234))==1122 ...

  6. sleep() 和 wait() 有什么区别?

     sleep:Thread类中定义的方法,表示线程休眠,会自动唤醒: wait:Object中定义的方法,需要手工调用notify()或者notifyAll()方法. sleep是线程类(Thread ...

  7. 汉化 android studio

    Analyze APK...android.jar\com\android\tools\idea\apk\viewer AnalyzeApkAction.class

  8. 潭州课堂25班:Ph201805201 并发(进程与线程池) 第十四课 (课堂笔记)

    循环执行一个线程 # -*- coding: utf-8 -*- # 斌彬电脑 # @Time : 2018/7/20 0020 5:35 import threading import queue ...

  9. java后端发送请求

    package com.ty.mapapisystem.util; import java.io.BufferedReader;import java.io.FileInputStream;impor ...

  10. struts2返回json数据

    <!-- ajax注册 --> <package name="jsonstruts2" namespace="/json" extends=& ...