在GBA上寫光線追蹤:定點數運算庫

来源:https://www.cnblogs.com/h5l0/archive/2020/04/14/lib_hl_fixed-point.html
-Advertisement-
Play Games

這篇文章是關於我的GBA庫lib_hl中數學庫的定點數部分。 定點數是什麼?為什麼要用定點數? 在之前的文章中,我已經介紹了GBA的硬體,它的CPU竟然居然理所當然沒有浮點數運算單元! 我要寫的是光線追蹤程式,基本上都在做精確的數學運算,而這個CPU卻連浮點數都不支持,那不是沒得玩? 當然是有方法的 ...


這篇文章是關於我的GBA庫lib_hl中數學庫的定點數部分。

 


 

定點數是什麼?為什麼要用定點數?

在之前的文章中,我已經介紹了GBA的硬體,它的CPU竟然居然理所當然沒有浮點數運算單元!

我要寫的是光線追蹤程式,基本上都在做精確的數學運算,而這個CPU卻連浮點數都不支持,那不是沒得玩?

當然是有方法的:

1、使用軟體浮點數,在軟體層面模擬浮點數,但比起硬體浮點數慢了太多,光線追蹤是運算密集型程式,這樣肯定不行;

2、使用定點數,在電腦普遍沒有浮點運算單元時,大家都是用定點數代替小數運算。乘除法速度只比整數運算慢幾倍,還是可以接受的。

定點數通過固定小數點位置,使用整數表示小數,與相比浮點數,定點數可表示範圍比浮點數小,而且它的表示範圍和精度不可兼得。

關於定點數的詳細原理,參見我的另一篇文章(不打算寫了),可以百度

 

hl_types.h

最開始寫的是一個.h頭文件,裡面包含了將用到的數據類型和一些常見操作的巨集定義。

無論是在什麼程式中,對數據類型進行定義是非常必要的,因為int, long, long long這些類型在不同的編譯器中長度是不同的,在32位/64位的情況下也是不同的,為了程式的強適應性,應該使用自己定義的長度可知的數據類型。

基礎數據類型 代碼如下:

typedef signed    char    s8;  //8位正負整數
typedef signed    short     s16;  //16位正負整數
typedef signed    int     s32;  //32位正負整數
typedef signed    long long s64;  //64位正負整數

typedef unsigned char       u8;   //8位正整數
typedef unsigned short     u16;    //16位正整數
typedef unsigned int       u32;    //32位正整數
typedef unsigned long long u64;  //64位正整數

typedef volatile signed      char     vs8;    //8位正負整數
typedef volatile signed      short   vs16;    //16位正負整數
typedef volatile signed      int     vs32;    //32位正負整數

typedef volatile unsigned    char     vu8;    //8位正整數
typedef volatile unsigned    short   vu16;    //16位正整數
typedef volatile unsigned    int     vu32;    //32位正整數

然後是一些會隨著32/64位系統變化的類型:

#ifdef _X64
typedef long long _stype;
typedef unsigned long long _utype;
#define _XLEN        8
#else
typedef int _stype;
typedef unsigned int _utype;
#define _XLEN        4
#endif

//通用指針類型
typedef void *t_pointer, *t_ptr;
//整型地址類型
typedef _utype t_addr;

通過在64位環境下預定義一個_X64的巨集,可以使_utype在32位時是4位元組,64位數時是8位元組長。雖然我們的GBA肯定是32位的,但假如我們要把程式遷移到64位電腦上,就要註意指針類型和地址的長度變化。

然後是一些常用的定義:

//布爾類型
typedef int Bool;

#ifndef NULL
#define NULL        0
#endif

#ifndef TRUE
#define TRUE        1
#endif

#ifndef FALSE
#define FALSE        0
#endif

/*內聯函數聲明*/
#define _INLINE_ static inline

/*獲取元素相對結構體起始地址的偏移*/
#define _OFFSET(_type,_element) ((t_addr)&(((_type *)0)->_element))
...
#define BIT(n)    (1<<(n))        //第n比特為1 (2^n)
...
#define SETFLAG(v,flag)        v=(v|flag)    //設定Flag
#define HASFLAG(v,flag)        (v&flag)        //是否有Flag
#define HASFLAGS(v,flags)    ((v&(flags))==(flags))    //是否有全部flags
#define NOFLAG(v,flag)        ((v&flag)==0)    //沒有Flag
...

其他定義後續我們需要時在補上,現在我們可以開始寫數學庫了。

 

hl_math.h

文件開頭加上:

#pragma once
#include <hl_system.h>

#pragma once是寫給編譯器看的,意思是這段代碼只編譯一次。

之所以要在頭文件加這句話,是因為C中引用頭文件,是通過直接把頭文件的記憶體複製到#include的位置,如果在多個文件中都包含了同一個頭文件,編譯時就會導致巨集、結構體等被多次定義,引起編譯錯誤。

另一種適合所有編譯器的寫法是:

#ifndef _XXX_H 
#define _XXX_H 
代碼 ...
#endif

定義定點數

之後開始編寫真正的代碼,先定義定點數類型:

//32位定點數
typedef s32 fp32;

//32位定點數的小數位數 20bit
//整數大小 -2048-2047,小數精度 0.000001
#define FP32_FBIT    20
#define FP32_1 (1<<FP32_FBIT) //fp32 1f #define FP32_H5 (1<<(FP32_FBIT-1)) //fp32 0.5f #define FP32_LIMIT1 (FP32_1-1) //fp32 不到1f的最大值 #define FP32_MAX 2147483647 #define FP32_MIN (-2147483647-1) #define FP32_MAXINT ( (1<<(31-FP32_FBIT))-1) #define FP32_MININT (-(1<<(31-FP32_FBIT))) #define FP32_PI (1686629713>>(29-FP32_FBIT)) #define FP32_SQRT2 (1518500249>>(30-FP32_FBIT)) #define FP32_SQRT3 (1859775393>>(30-FP32_FBIT)) #define FP32_F2(n) (1<<(FP32_FBIT-(n))) //fp32 1/(2^n) //16位定點數 typedef s16 fp16; //16位定點數的小數位數 10bit //整數大小 -32-31,小數精度 0.001 #define FP16_FBIT   10
#define FP16_1   (1<<FP16_FBIT) #define FP16_H5  (1<<(FP16_FBIT-1)) #define FP16_MAX 32767 #define FP16_MIN   -32768 #define FP16_MAXINT ( (1<<(15-FP16_FBIT))-1) #define FP16_MININT (-(1<<(16-FP16_FBIT)))

可以看到我的定點數有32位的和16位的,32位叫fp32,主要用於精度要求比較高的大部分運算,16位的叫fp16,主要用於精度低的色彩等運算。

fp32為了運算精度,給小數部分分配了20位(可以說是非常重視精度),這樣小數的分度值是1/220 ,到小數點後6位的精度,而整數只有12位,除去符號位,可表示211=2048,範圍就是-2048~2047。

fp16的位長有效,給小數分配10位,也只有1/210=1/1024也就是0.001的精度,而整數只剩可憐的5位,範圍是-32~31。

除了定義小數位長FBIT,我還定義了一些常見數值的對應的定點數,例如1,0.5,π。可以看到,定點數的1就是1*220,0.5就是0.5*220,這就是定點數的原理。

同樣的原理我們可以寫幾個轉換函數:

//int -> fp32
static inline fp32    fp32_int(int n) { return n << FP32_FBIT; }
//float -> fp32
//static inline fp32    fp32_float(float f) { return (fp32)(f * (1 << FP32_FBIT)); }
//int/100 -> fp32
static inline fp32    fp32_100f(int n) { return (((s64)n << FP32_FBIT) + 50) / 100; }
//fp32 -> int
static inline int    int_fp32(fp32 f) { return f >> FP32_FBIT; }

看完代碼對定點數的理解應該也深一些吧。

所有函數前都加上了static inlineinline是聲明這個函數是內聯函數,也就是在編譯時會被展開,避免函數調用開銷,對於我們這種常用且短小的運算函數,當然要加。但inline只是向編譯器提個建議,編譯器可能不聽,如果它覺得這個函數太大,內聯不划算,就不內聯了。這時這個函數就變成了定義在頭文件的普通函數,這會帶來一個問題,如果頭文件被多次包含會導致函數重定義,所以加上static,聲明為靜態函數,只是在聲明它的文件中可見,避免命名衝突。其實,規範地寫,應該使用之前定義的_INLNE_,以防切換到不支持staic inline特性的編譯器。

定點數運算

之後就是運算函數了。首先是加減運算,和整數運算並無兩樣。它的運算原理如下:

假設:

整數A是小數a的定點數形式,即 A = a*fs (fs = 1<<FBIT)

整數B是小數b的定點數形式,即 B = b*fs (fs = 1<<FBIT)

則 定點數A 加 定點數B 的公式是:

 A (+) B = a*fs (+) b*fs = (a+b)*fs = (A/fs+B/fs)*fs = A+B

//fp32 + fp32 **事實上沒有用的必要
static inline fp32    fp32_add(fp32 a, fp32 b) { return a + b; }
//fp32 - fp32 **事實上沒有用的必要
static inline fp32    fp32_sub(fp32 a, fp32 b) { return a - b; }

 

然後是乘除法:

 先看代碼,區別是乘完後需要縮小2FBIT,除完後需要放大2FBIT

//fp32 * fp32 (64位安全運算)
static inline fp32    fp32_mul64(fp32 a, fp32 b)
{ return (((s64)a) * b) >> FP32_FBIT; }

//fp32 / fp32 (64位安全運算) *b<1仍可能溢出
static inline fp32    fp32_div64(fp32 a, fp32 b)
{ return (((s64)a) << FP32_FBIT) / b; }

定點數A乘定點數B的推導過程:

 A (x) B = (a*b)*fs = (A/fs)*(B/fs)*fs = (A*B)/fs

定點數A除定點數B的推導過程:

 A (÷) B = (a/b)*fs = (A/fs)/(B/fs)*fs = (A/B)*fs

不難理解,定點數是小數乘了2FBIT得到的,如果兩個定點數相乘,兩次2FBIT就累積了,所以要除去一次2FBIT

 

之後是一些常用的函數:

//fp32^2
static inline fp32    fp32_pow2(fp32 a)
{ return (((s64)a) * a) >> FP32_FBIT; }
//返回結果是u64
static inline u64        fp32_pow2_64(fp32 a)
{ return (((s64)a) * a) >> FP32_FBIT; }

static inline fp32    fp32_lerp(fp32 a, fp32 b, fp32 t)
{ return a + fp32_mul64(b - a, t); }

下一部分 數學函數庫 也會包含一些定點數常用函數,例如開方和三角函數。

這裡只列出小部分,其他若有需要請看源碼。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • Python安裝 想要使用Django,前提是要安裝好Python軟體及運行環境,因為Django是基於Python寫的。具體Python安裝過程詳見: "Python3 環境搭建" 查看安裝的Python版本 shell $python m pip install django i https:/ ...
  • 背景 公司賣了一個產品給甲方,甲方要求部署後,要以 來訪問。甲方提供了證書信息和私鑰,記錄一下部署過程。 實現 1、思路 在我們產品伺服器上部署一個 、證書信息也放在這個伺服器上。外界的 經過 變成 協議,大致思路如下: 2、安裝過程 (1)上傳證書、私鑰到伺服器 證書 放於 ; 私鑰 放於 ; ( ...
  • 並查集最常用來判斷圖是否為連通圖,或用來求圖的連通分量數。 並查集1--<=>求連通分量個數 題目描述 某省調查城鎮交通狀況,得到現有城鎮道路統計表,表中列出了每條道路直接連通的城鎮。省政府“暢通工程”的目標是使全省任何兩個城鎮間都可以實現交通(但不一定有直接的道路相連,只要互相間接通過道路可達即可 ...
  • Redis的全稱是:Remote Dictionary.Server,本質上是一個Key-Value類型的記憶體資料庫,很像 memcached,整個資料庫統統載入在記憶體當中進行操作,定期通過非同步操作把資料庫數據flush到硬碟 上進行保存。 因為是純記憶體操作,Redis的性能非常出色,每秒可以處理超... ...
  • 美團java一面問題 自我介紹 項目相關 線程與進程的區別 進程線程間的通信方式 hashmap與concurrenthashmap的區別 資料庫的事務 資料庫索引有哪些 大文件統計每個字元串的詞頻 有什麼想問的 一面面完之後面試官讓我回去等通知,一度以為掛了,沒想到出門沒有一個小時收到了美團2面的 ...
  • 前言 雖說已經工作,並且也做了兩個項目,但總覺得自己的基礎知識不牢固,所以從頭開始學起。學習過程中的一些代碼已上傳到 "Github" 和 "碼雲" 基礎知識 認識 PHP 略。。。 安裝與配置 略。。。 語言基礎 標記風格 XML 風格 腳本風格 簡短風格 ASP 風格 如果使用簡短風格和 ASP ...
  • 通俗理解spring源碼(二)—— 資源定位與載入 最開始學習spring的時候,一般都是這樣用: ApplicationContext context = new ClassPathXmlApplicationContext("spring.xml"); User user = (User)con ...
  • 1. 前言 首先自我介紹一下,我是一個做 Java 的開發人員,從今年下半年開始,一直在各大技術博客網站發表自己的一些技術文章,差不多有幾個月了,之前在 cnblog 博客園加了網站統計代碼,看到每天的訪問量逐漸多了起來,國慶正好事情不多,就想著寫一個爬蟲,看下具體閱讀量增加了多少,這也就成了本文的 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...