2009年3月6日星期五

Accumulative Screen Space Ambient Occlusion

This is my attempt to combine Real-Time Reprojection Cache and Screen Space Ambient Occlusion. Using such caching scheme, the spatio-temporal coherence nature of the SSAO algorithm can be exploited. You can download the demo with shader source here.


Add Video
The name "Accumulative SSAO" comes from the fact that the occlusion value is accumulated and averaged over a number of frames. The algorithm itself is quite independent of how the occlusion is calculated and here I will assume the reader is familiar with SSAO implementation such as those from Crysis and Startcraft II.

The pipeline

For every frame,

  1. The scene was rendered using deferred shading technique, producing the color, normal and depth buffers.
  2. A number of random vectors were generated in CPU (where in usual SSAO these vectors only generated once in the program).
  3. The normal and depth buffer are then utilized to calculate the occlusion value in the SSAO pass.
  4. Instead of writing the occlusion value to the final output, it would combine with the previous frame's accumulated occlusion value and then written to a second accumulation buffer.
  5. A blur pass can optionally apply to the most updated accumulated occlusion buffer.
  6. The color buffer was then combined with the occlusion value to product the final result, also the two accumulation buffers were switched with each other.
Re-projection

The re-projection happens in the SSAO pass when it tries to access the previous frame's occlusion value. Having the eye-space 3d position for each pixel, we can transform that into a texture coordinate by using a matrix (and a perspective division afterward), lets call it the delta matrix. This matrix is calculated on CPU as:
bias = translation(0.5, 0.5, 0.5) * scale(0.5, 0.5, 0.5)
deltaMatrix = bias * lastFrameProjection * lastFrameView * currentFrameView.inverse()

In simple words, for each current frame's pixel, we are trying to locate their corresponding pixel coordinate on the last frame. If there is no camera movements, the two coordinates should be the same.

Accumulative AO

With the re-projection working, the current frame's occlusion value can be combined with the previous one with the following accumulation formula:
currentAo = currentAo / 30.0 + lastFrameAo * 29.0 / 30.0;

In order to make something interesting for the above equation, the current occlusion value should not be the same as the previous one. Therefore, a new set of sampling position should be generated for each frame, this can be done by re-generating the random unit sphere samples or the dithering texture every frame. In a loosely sense, it is doing a Monte Carlo Integration over the time domain. To achieve better visual quality, more frames should be taken over the time.

As each frame's AO value will also depends on the last few frames, there will be some time delay for the AO to become up-to-date in a dynamic scene. However, by changing the numerator and denominator in the equation, the trade-off between quality and responsiveness can be adjusted.

Cache-miss consideration

Up to now the cache miss problem of the re-projection is not yet addressed. A cache miss will happen if somewhere in the scene that cannot be seen before becoming visible now, due to camera or object movement. Such a cache miss can be detected by comparing the current pixel's depth value with it's re-projected counterpart. If the two values differed by a certain threshold, a cache miss is detected. And to do this, the last frame's depth value is needed. Instead of using a separated texture to store the last frame's depth value, the depth can be encoded and stored together with the accumulative AO value into a 32-bit texture.
// Encode a float value into 3 bytes
// The input value should be in the range of [0, 1
// Reference: http://www.ozone3d.net/blogs/lab/?p=113
vec3 packFloatToVec3i(const float value)
{
 const vec3 bitSh = vec3(256.0 * 256.0, 256.0, 1.0);
 const vec3 bitMsk = vec3(0.0, 1.0/256.0, 1.0/256.0);
 vec3 res = fract(value * bitSh);
 res -= res.xxy * bitMsk;
 return res;
}
float unpackFloatFromVec3i(const vec3 value)
{
 const vec3 bitSh = vec3(1.0/(256.0*256.0), 1.0/256.0, 1.0);
 return dot(value, bitSh);
}

If there was a cached miss, the accumulative AO will be discarded and the instance AO value is used instead. Of course more samples can be taken in this frame to reduce the visual impact of the cache miss.

Discussion/improvements
  • Currently a new independent set of random samples were generated for the above video demo. Other random sample over time generation method may reduce the noise.
  • As some of the re-projection cache scheme suggested, a cache value should be cleared after a certain period of time to avoid in-stability and provide a better response to dynamic environment, and this is done here by the accumulation formula.
  • To reduce cache miss due to object movement, each object's last transformation matrix can also be incorporated into the algorithm.
  • The depth encoding scheme also make the blur pass much more efficient.
Conclusion

The explained algorithm provides a new way to improve the quality and efficiency of traditional SSAO by using the result from a number of frames instead of one. It also opens up more parameters and sampling patterns to explore with.

2009年2月17日星期二

用 C# 製作使用者界面

過去兩個星期開始製作使用者界面,選擇了 C# 加 Microsoft Windows Form 作為平台。
除了 C# 本身容易使用外,最令我覺得欣慰的就是那龐大的使用者社區,好一些想要的功能都已經有其他人做好了,或已有詳盡的教學可供參考。
以下是一些在我製作 Studio Tool 的時候所用到的一些 library:

2009年2月16日星期一

移除 ".svn" 文件夾

話說有一天我想著怎樣把一個專案之下的 ".svn" 文件夾全部移除,想著想著的都是一些 Linux command。最後找到這個:

find -type d -name .svn -exec rm -rf {} \;

怎知原來每天使用的 Tortoise SVN 已經有這個功能... 又做了一件愚蠢的事 XD

2009年2月4日星期三

九型人格分析

看來我滿喜歡建造遊戲引擎來成全自己亦希望同時成就他人。

九型人格分析
第二型助人者、全愛型、助人型、成就他人者、博愛型
14%
第一型完美主義者、完美型、改革者、改進型、秩序大使
13%
第六型忠誠型、忠誠型、尋找安全者、謹慎型
13%
第三型成就者、事業型、成就型、實踐型
12%
第四型藝術型、浪漫者、自我型、憑感覺者
11%
第五型智慧型、觀察者、思想型、理性分析者、思考型
9%
第七型快樂主義型、豐富型、活躍型、創造可能者、享樂型
9%
第八型領袖型、能力型、挑戰者、保護者、權威型
9%
第九型和平型、和平者、和諧型、維持和諧者
7%

2009年1月15日星期四

SSAO Demo

花了一點時間整理好我的 SSAO Demo,請按這裡下載。請各位多給意見 :)

操縱方法:
  • 鏡頭移動:W、A、S、D、Page up、Page down
  • 鏡頭方向:滑鼠左鍵
  • SSAO開關:F1,默認值:開
  • 銀幕減半開關:F2,默認值:開
  • 增大/減少模糊操作次數:F3/Shift+F3,默認值:2
  • 漫射材質開關:F4,默認值:關
  • 增大/減少閉塞半徑:Shift+F5/F5
後記:
  • 看過 R5 Demo 後才覺悟增大 blur pass 次數和 blur kernel size 的分別。 Orz
  • 把 depth encode 到三個 8-bit integer,那麼一塊 32-bit 的 RGBA render target 就可以運載 occlusion value 和 depth 到模糊操作中,從而大大減少 texture fetch 的數量。
相關文章

2009年1月13日星期二

檔案監視器

看過了猴子靈藥的 "Database Hot Loader" 後,心動之下又想做些類似的東西,哪就是一個用來監視檔案系統的小工具。有了它,遊戲裡的任何素材(美術素材,以及音效、字型、腳本程序等等)檔案一經修改就會立即在遊戲裡反映出來,因而省去重新啟動遊戲程式的煩厭。當然背後還需健全的資源系統才能成事。

在視窗環境中,標準的方法是調用 FindFirstChangeNotification 或 ReadDirectoryChangesW;前者告訴你某個資料夾有否被更改,後者還會告訴你甚麼檔案/資料夾曾被更改。怎知 ReadDirectoryChangesW 的調用殊不簡單,MSDN 又沒有範例,上 CodeProject 碰碰運氣得來的是一個 3000 多行代碼的類別,Google 一翻還是找不到想要的。

經過一翻努力和嘗試(我相信 MSDN 是有錯漏的),得知 GetOverlappedResult 和 ReadDirectoryChangesW 的配合是最簡單的;無須和任何多緒有關的東西打交道。我的 FileMonitor 就只有 constructor 和 getChangedFile 這兩個函數。

FileMonitor.h

#ifndef __FILEMONITOR__
#define __FILEMONITOR__

#include <string>

/*! To monitor file changes under a particular folder.
The implementation use the win32 ReadDirectoryChangesW() function
with GetOverlappedResult() to perform the monitoring, therefore
no thread is created and so making the interface very simple.

With the limitation of ReadDirectoryChangesW() is using a fixed buffer
to hold the information between calls of getChangedFile(), FileMonitor
may fail to detect file changes between calls of getChangedFile() if
the file names are too large to fit into the buffer. To overcome the
issue, you need to call getChangedFile() frequently.

\sa http://mtlung.blogspot.com/2009/01/blog-post.html

Example:
\code
FileMonitor monitor(L"pathToMonitor", true);
// In your main loop:
while(true) {
std::wstring path;
while(!(path = monitor.getChangedFile()).empty()) {
std::wcout << path << std::endl;
}
}
\endcode
*/
class FileMonitor
{
// FileMonitor is non-copyable
FileMonitor(const FileMonitor&);
FileMonitor& operator=(const FileMonitor&);

public:
/*! Constructor
\param path The path to monitor
\param recursive Watch the path recursively
\param operationTowatch Which file operation to monitor with,
having the same meaning as the dwNotifyFilter in ReadDirectoryChangesW()
function, see http://msdn.microsoft.com/en-us/library/aa365465(VS.85).aspx
for more details. By default, it's value is -1 and have the same meaning
as FILE_NOTIFY_CHANGE_LAST_WRITE
*/
FileMonitor(const wchar_t* path, bool recursive, int operationTowatch = -1);

~FileMonitor();

/*! Get which file under the watching directory is changed.
This function is non-blocking and if there is no changes in the
file system, it will simple return an empty string.

\note
The current implementation use a fixed buffer to capture all the
file changes between calls of getChangedFile(). If there are too
much changes or the file names get too long, the buffer overflow
and that file change notification will lost.
See more on the documentation of ReadDirectoryChangesW() in MSDN.
*/
std::wstring getChangedFile() const;

private:
class Impl; //!< Private implementation class
Impl* mImpl;
}; // FileMonitor

#endif // __FILEMONITOR__

FileMonitor.cpp

#include "FileMonitor.h"
#include <assert.h>
#include <iostream>
#include <list>

// Exclude rarely-used stuff from Windows headers
#ifndef WIN32_LEAN_AND_MEAN
# define WIN32_LEAN_AND_MEAN
#endif
#ifndef VC_EXTRALEAN
# define VC_EXTRALEAN
#endif
#include <windows.h>

#ifdef _MSC_VER // Currently only windows is supported

class FileMonitor::Impl
{
public:
Impl(const wchar_t* path, bool recursive, int operationTowatch)
: mRecursive(recursive), mOperationTowatch(operationTowatch)
{
assert(int(mBuffer) % 4 == 0 && "Address of mBuffer must be 4-byte aligned");

// Adjust the defalt value for mOperationTowatch
if(mOperationTowatch == -1)
mOperationTowatch = FILE_NOTIFY_CHANGE_LAST_WRITE;

mDirectory = ::CreateFileW(
path,
FILE_LIST_DIRECTORY,
FILE_SHARE_DELETE | FILE_SHARE_READ | FILE_SHARE_WRITE,
0,
OPEN_EXISTING,
// ReadDirectoryChangesW() needs FILE_FLAG_BACKUP_SEMANTICS
FILE_FLAG_OVERLAPPED | FILE_FLAG_BACKUP_SEMANTICS,
0
);

memset(&mOverlapped, 0, sizeof(mOverlapped));
if(!mDirectory || !readChange()) {
::CloseHandle(mDirectory);
mDirectory = NULL;
std::wcerr << L"Fail to watch directory: " << path << std::endl;
}
}

~Impl()
{
::CloseHandle(mDirectory);
}

bool readChange() const
{
return ::ReadDirectoryChangesW(
mDirectory,
mBuffer, sizeof(mBuffer),
mRecursive,
mOperationTowatch,
NULL, // bytesRetured
&mOverlapped,
0 // callBack
) != 0;
}

std::wstring getChangedFile() const
{
// We will try to call GetOverlappedResult() even there are entries inside
// mFiles, so that it's less possible for the mBuffer to be overflowed.

// For some unknown reason(s) ReadDirectoryChangesW() will report the file twice,
// therefore we add a loop to filter out those duplicated entries.
for(size_t i=2; i--;)
{
DWORD bytesRetured = 0;
if(0 == ::GetOverlappedResult(mDirectory, &mOverlapped, &bytesRetured, false))
goto CACHED; // The use of goto here makes the code clean.

if(bytesRetured == 0) {
// TODO: To reduce the chance of insufficient buffer,
// we can move the code to another thread.
std::wcerr << L"Error returned by ReadDirectoryChangesW(), "
L"most likely the internal buffer is too small" << std::endl;
readChange();
goto CACHED;
}

FILE_NOTIFY_INFORMATION* p = reinterpret_cast<FILE_NOTIFY_INFORMATION*>(mBuffer);
while(true)
{
std::wstring fileName(p->FileName, p->FileNameLength / sizeof(wchar_t));

// Skip duplicated entry
if(mFiles.empty() || fileName != mFiles.back())
mFiles.push_back(fileName);

if(p->NextEntryOffset == 0)
break;

p = reinterpret_cast<FILE_NOTIFY_INFORMATION*>((char*)p + p->NextEntryOffset);

// Do some extra buffer overflow check.
if((char*)p - (char*)mBuffer > sizeof(mBuffer))
break;
}

if(!readChange())
return L"";
}

CACHED:
if(!mFiles.empty()) {
std::wstring ret = mFiles.front();
mFiles.pop_front();
return ret;
}

return L"";
}

HANDLE mDirectory;
bool mRecursive;
int mOperationTowatch;
/*! This buffer must be 4-byte aligned, therefore we use int as the type.
You may change the buffer size to fit your needs.
*/
mutable int mBuffer[2048];
mutable OVERLAPPED mOverlapped;
//! A list of wstring acting as a circular buffer.
mutable std::list<std::wstring> mFiles;
}; // Impl

FileMonitor::FileMonitor(const wchar_t* path, bool recursive, int operationTowatch)
{
mImpl = new Impl(path, recursive, operationTowatch);
}

FileMonitor::~FileMonitor()
{
delete mImpl;
}

std::wstring FileMonitor::getChangedFile() const
{
__assume(mImpl); // We know mImpl is always not null, shut off the C++ analysis warning
return mImpl->getChangedFile();
}

#endif // _MSC_VER

Main.cpp

#include "FileMonitor.h"
#include <iostream>
#include <conio.h> // For _kbhit()

int main()
{
FileMonitor monitor(L"./", true);

std::wcout << L"Create and modify the files in the current directory, "
L"and the FileMonitor will tell you the name of those files.";
std::wcout << L" Press any key to quit the program" << std::endl;

while(!_kbhit())
{
std::wstring path;
// Keep polling the monitor, But a real application should
// only poll the monitor once a while.
while(!(path = monitor.getChangedFile()).empty())
{
std::wcout << path << std::endl;
}
}

return 0;
}

2008年12月31日星期三

新年前的 SSAO

相信很多朋友都趁年尾來一篇日誌,我也來熱鬧一番 ^.^
上一篇的 SSAO 日誌距今已有三個月;之後更改了演算法,本想多作改進和包裝成 demo 才放進來,但現在手頭上還正在建造一個給 OpenGl 的 Effect 架構,此刻只好放些 Screen shot 好了。

演算法和 星海爭霸2 的相近,但我對 Image processing 的學識尚淺,Bilateral (edge-preserving) blur 的部分還有問題。

最後祝大家新年快樂。


沒有SSAO, 900 fps


屏幕 1/4 SSAO with dither, 252 fps


屏幕 1/4 SSAO with dither + blur, 233 fps