【遊戲框架系列】簡單的圖形學(三)——光源
參考自:用JavaScript玩轉計算機圖形學(二)基本光源 - Milo Yip - 博客園,主要講述三種最基本的光源——平行光、點光源、聚光燈,其實就是三種數學模型。
代碼的調整
先前的代碼中,顏色是由幾何物體自身計算得出,因此使用很有限。在Phong材質中,顯示的效果已經很不錯了,然而Phong材質是要假定有一個光源的。我們的代碼需要從以面向物體渲染為面向光源渲染。
新的邏輯:https://github.com/bajdcc/GameFramework/blob/master/CCGameFramework/base/pe2d/Render2DLight.cpp
主邏輯 代碼:
void PhysicsEngine::RenderLightIntern(World& world, const PerspectiveCamera& camera, BYTE* buffer, cint width, cint height)n{n for (auto y = 0; y < height; y++)n {n const auto sy = 1.0f - (1.0f * y / height);nn for (auto x = 0; x < width; x++)n {n const auto sx = 1.0f * x / width;nn // sx和sy將屏幕投影到[0,1]區間nn // 產生光線n const auto ray = camera.GenerateRay(sx, sy);nn // 測試光線與球是否相交n auto result = world.Intersect(ray);n if (result.body)n {n color color;n for (auto & k : world.lights) { // 這裡不一樣了n auto lightSample = k->Sample(world, result.position);nn if (!lightSample.empty()) {n auto NdotL = DotProduct(result.normal, lightSample.L); // 計算角度nn // 夾角為銳角,光源在平面前面n if (NdotL >= 0)n // 累計所有光線n // NdotL 就是光源方向在法向量上的投影n color = color + (lightSample.EL * NdotL);n }n }n buffer[0] = BYTE(color.b * 255);n buffer[1] = BYTE(color.g * 255);n buffer[2] = BYTE(color.r * 255);n buffer[3] = 255;n }n elsen {n // 沒有接觸,就是背景色n buffer[0] = 0;n buffer[1] = 0;n buffer[2] = 0;n buffer[3] = 255;n }nn buffer += 4;n }n }n}n
簡單來說,就是求出交點時,做:
- 計算各大光源基於該點的光線顏色和光線方向
- 計算光線在交點法向量上的投影,作為顏色混合比的依據
- 將所有顏色累計起來
下面介紹三種光源
平行光

平行光的屬性:
// 平行光nclass DirectionalLight : public Lightn{npublic:n DirectionalLight(color irradiance, vector3 direction);nn LightSample Sample(World& world, vector3 position) override;nn color irradiance; // 幅照度n vector3 direction; // 光照方向n vector3 L; // 光源方向n};nnDirectionalLight::DirectionalLight(color irradiance, vector3 direction)n : irradiance(irradiance), direction(direction)n{n L = -Normalize(direction);n}nnLightSample DirectionalLight::Sample(World& world, vector3 position)n{n static LightSample zero;nn if (shadow) {n const Ray shadowRay(position, L);n const auto shadowResult = world.Intersect(shadowRay);n if (shadowResult.body)n return zero;n }nn return LightSample(L, irradiance); // 就返回光源顏色n}n
這裡注意L是光源方向單位向量。
平行光我們只需要知道光源方向和光源顏色就可以了。非常簡單,不用算投影,這是主邏輯的工作。
這裡說一下陰影,平行光有陰影,當從交點向光源方向看時,如果中間有障礙物,就返回黑色。
點光源

// 點光源nclass PointLight : public Lightn{npublic:n PointLight(color intensity, vector3 position);nn LightSample Sample(World& world, vector3 position) override;nn color intensity; // 幅射強度n vector3 position; // 光源位置n};nnstatic LightSample zero;nnLightSample PointLight::Sample(World& world, vector3 pos)n{n // 計算L,但保留r和r^2,供之後使用n const auto delta = position - pos; // 距離向量n const auto rr = SquareMagnitude(delta);n const auto r = sqrtf(rr); // 算出光源到pos的距離n const auto L = delta / r; // 距離單位向量nn if (shadow) {n const Ray shadowRay(pos, L);n const auto shadowResult = world.Intersect(shadowRay);n // 在r以內的相交點才會遮蔽光源n // shadowResult.distance <= r 表示:n // 以pos交點 -> 光源位置 發出一條陰影測試光線n // 如果陰影測試光線與其他物體有交點,那麼相交距離 <= rn // 說明pos位置無法直接看到光源n if (shadowResult.body && shadowResult.distance <= r)n return zero;n }nn // 平方反比衰減n const auto attenuation = 1 / rr;nn // 返回衰減後的光源顏色n return LightSample(L, intensity * attenuation);n}n
點光源有一個平方反比衰減規律,故而要先算光源到交點pos的距離r,然後求出L,實際上L就是光源位置到交點的方向單位向量。接著要計算顏色,點光源本身顏色intensity,由於有衰減,因此變成了intensity * attenuation。
再說下陰影,如何計算點光源的陰影?這比平行光複雜些。從交點處向光源位置發出一條光線,如果當中有障礙物,那麼被遮擋,返回黑色(就是遮擋測試)。
聚光燈

// 聚光燈nclass SpotLight : public Lightn{npublic:n SpotLight(color intensity, vector3 position, vector3 direction, float theta, float phi, float falloff);nn LightSample Sample(World& world, vector3 position) override;nn color intensity; // 幅射強度n vector3 position; // 光源位置n vector3 direction; // 光照方向n float theta; // 內圓錐的內角n float phi; // 外圓錐的內角n float falloff; // 衰減nn /* 以下為預計算常量 */n vector3 S; // 光源方向n float cosTheta; // cos(內圓錐角)n float cosPhi; // cos(外圓錐角)n float baseMultiplier;// 1/(cosTheta-cosPhi)n};nnSpotLight::SpotLight(color intensity, vector3 position, vector3 direction, float theta, float phi, float falloff)n : intensity(intensity), position(position), direction(direction), theta(theta), phi(phi), falloff(falloff)n{n S = -Normalize(direction);n cosTheta = cosf(theta * float(M_PI) / 360.0f);n cosPhi = cosf(phi * float(M_PI) / 360.0f);n baseMultiplier = 1.0f / (cosTheta - cosPhi);n}nnLightSample SpotLight::Sample(World& world, vector3 pos)n{n // 計算L,但保留r和r^2,供之後使用n const auto delta = position - pos; // 距離向量n const auto rr = SquareMagnitude(delta);n const auto r = sqrtf(rr); // 算出光源到pos的距離n const auto L = delta / r; // 距離單位向量nn /*n * spot(alpha) =n *n * 1n * where cos(alpha) >= cos(theta/2)n *n * pow( (cos(alpha) - cos(phi/2)) / (cos(theta/2) - cos(phi/2)) , p)n * where cos(phi/2) < cos(alpha) < cos(theta/2)n *n * 0n * where cos(alpha) <= cos(phi/2)n */nn // 計算spotn auto spot = 0.0f;n const auto SdotL = DotProduct(S, L);n if (SdotL >= cosTheta)n spot = 1.0f;n else if (SdotL <= cosPhi)n spot = 0.0f;n elsen spot = powf((SdotL - cosPhi) * baseMultiplier, falloff);nn if (shadow) {n const Ray shadowRay(pos, L);n const auto shadowResult = world.Intersect(shadowRay);n // 在r以內的相交點才會遮蔽光源n // shadowResult.distance <= r 表示:n // 以pos交點 -> 光源位置 發出一條陰影測試光線n // 如果陰影測試光線與其他物體有交點,那麼相交距離 <= rn // 說明pos位置無法直接看到光源n if (shadowResult.body && shadowResult.distance <= r)n return zero;n }nn // 平方反比衰減n const auto attenuation = 1 / rr;nn // 返回衰減後的光源顏色n return LightSample(L, intensity * (attenuation * spot));n}n
聚光燈是非常複雜的數學模型,我們不去探究為什麼公式這樣的,只要實現就行。
純數學計算不多講,這裡主要有一個spot(聚光燈係數),所以最後的顏色是intensity * (attenuation * spot)。其它跟點光源的實現也差不多。
三原色混合

原想這東西怎麼實現啊,現在想通了,就是在某點處(plane上一點)三個聚光燈打上去,將最終的顏色混合起來(加起來)。
簡單表述:三個光源的光分別為RGB(255,0,0)、RGB(0,255,0)、RGB(0,0,255),混合起來,加一下就是RGB(255,255,255),白色。
看到用JavaScript玩轉計算機圖形學(二)基本光源 - Milo Yip - 博客園 中的一個問題:
如果,幅射強度是負值的話,會怎麼樣?(雖然未證實反光子(antiphoton)的存在,但讀者能想到圖形學上的功能么?)
感覺就是PS中的正片疊底啊,見如何簡單的理解正片疊底和濾色?。
接下來會探討畫光的實現。
推薦閱讀:
※寫個自研的圖文混排(一)
※[OpenGL]Windows上OpenGL開發環境搭建
※如何判斷線段和圓弧是否相交?
※shadertoy 這個網站如何玩?
