Skip to content

Commit 25e600b

Browse files
committed
improve PBR
1 parent ecd2510 commit 25e600b

10 files changed

Lines changed: 509 additions & 158 deletions

File tree

jme3-core/src/main/java/com/jme3/environment/baker/GenericEnvBaker.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,9 @@ protected GenericEnvBaker(RenderManager rm, AssetManager am, Format colorFormat,
116116

117117
envMap = new TextureCubeMap(env_size, env_size, colorFormat);
118118
envMap.setMagFilter(MagFilter.Bilinear);
119-
envMap.setMinFilter(MinFilter.BilinearNoMipMaps);
119+
// Specular prefiltering samples the captured environment with explicit
120+
// source LODs, so the capture texture must keep a mip chain available.
121+
envMap.setMinFilter(MinFilter.Trilinear);
120122
envMap.setWrap(WrapMode.EdgeClamp);
121123
envMap.getImage().setColorSpace(ColorSpace.Linear);
122124
}

jme3-core/src/main/java/com/jme3/environment/baker/IBLGLEnvBaker.java

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,20 @@ public TextureCubeMap getIrradiance() {
119119
return irradiance;
120120
}
121121

122+
/**
123+
* Runtime samples the prefiltered map with sqrt(roughness) to recover a
124+
* normalized mip coordinate. Bake-time therefore needs the inverse mapping:
125+
* roughness = mipNorm^2.
126+
*/
127+
private float roughnessFromMip(int mip) {
128+
int mipCount = specular.getImage().getMipMapSizes().length;
129+
if (mipCount <= 1) {
130+
return 0f;
131+
}
132+
float mipNorm = (float) mip / (float) (mipCount - 1);
133+
return mipNorm * mipNorm;
134+
}
135+
122136
private void bakeSpecularIBL(int mip, float roughness, Material mat, Geometry screen) throws Exception {
123137
mat.setFloat("Roughness", roughness);
124138

@@ -171,7 +185,7 @@ public void bakeSpecularIBL() {
171185
int mip = 0;
172186
for (; mip < specular.getImage().getMipMapSizes().length; mip++) {
173187
try {
174-
float roughness = (float) mip / (float) (specular.getImage().getMipMapSizes().length - 1);
188+
float roughness = roughnessFromMip(mip);
175189
bakeSpecularIBL(mip, roughness, mat, screen);
176190
} catch (Exception e) {
177191
LOGGER.log(Level.WARNING, "Error while computing mip level " + mip, e);
@@ -187,7 +201,7 @@ public void bakeSpecularIBL() {
187201
specular.getImage().setMipmapsGenerated(true);
188202
if (sizes.length <= 1) {
189203
try {
190-
LOGGER.log(Level.WARNING, "Workaround driver BUG: only one mip level available, regenerate it with higher roughness (shiny fix)");
204+
LOGGER.log(Level.WARNING, "Workaround driver BUG: only one mip level is usable, regenerate mip 0 with roughness 1 to avoid an overly shiny fallback");
191205
bakeSpecularIBL(0, 1f, mat, screen);
192206
} catch (Exception e) {
193207
LOGGER.log(Level.FINE, "Error while recomputing mip level 0", e);
@@ -293,4 +307,4 @@ public void bakeIrradiance() {
293307

294308
}
295309

296-
}
310+
}

jme3-core/src/main/java/com/jme3/environment/baker/IBLHybridEnvBakerLight.java

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,20 @@ public boolean isTexturePulling() { // always pull textures from gpu
109109
return true;
110110
}
111111

112+
/**
113+
* Runtime samples the prefiltered map with sqrt(roughness) to recover a
114+
* normalized mip coordinate. Bake-time therefore needs the inverse mapping:
115+
* roughness = mipNorm^2.
116+
*/
117+
private float roughnessFromMip(int mip) {
118+
int mipCount = specular.getImage().getMipMapSizes().length;
119+
if (mipCount <= 1) {
120+
return 0f;
121+
}
122+
float mipNorm = (float) mip / (float) (mipCount - 1);
123+
return mipNorm * mipNorm;
124+
}
125+
112126
private void bakeSpecularIBL(int mip, float roughness, Material mat, Geometry screen) throws Exception {
113127
mat.setFloat("Roughness", roughness);
114128

@@ -161,7 +175,7 @@ public void bakeSpecularIBL() {
161175
int mip = 0;
162176
for (; mip < specular.getImage().getMipMapSizes().length; mip++) {
163177
try {
164-
float roughness = (float) mip / (float) (specular.getImage().getMipMapSizes().length - 1);
178+
float roughness = roughnessFromMip(mip);
165179
bakeSpecularIBL(mip, roughness, mat, screen);
166180
} catch (Exception e) {
167181
LOGGER.log(Level.WARNING, "Error while computing mip level " + mip, e);
@@ -177,7 +191,7 @@ public void bakeSpecularIBL() {
177191
specular.getImage().setMipmapsGenerated(true);
178192
if (sizes.length <= 1) {
179193
try {
180-
LOGGER.log(Level.WARNING, "Workaround driver BUG: only one mip level available, regenerate it with higher roughness (shiny fix)");
194+
LOGGER.log(Level.WARNING, "Workaround driver BUG: only one mip level is usable, regenerate mip 0 with roughness 1 to avoid an overly shiny fallback");
181195
bakeSpecularIBL(0, 1f, mat, screen);
182196
} catch (Exception e) {
183197
LOGGER.log(Level.FINE, "Error while recomputing mip level 0", e);
@@ -207,4 +221,4 @@ public void bakeSphericalHarmonicsCoefficients() {
207221
public Vector3f[] getSphericalHarmonicsCoefficients() {
208222
return shCoef;
209223
}
210-
}
224+
}

jme3-core/src/main/java/com/jme3/util/mikktspace/MikktspaceTangentGenerator.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,10 +148,13 @@ private static boolean generateTangents(Mesh mesh) {
148148
case Triangles:
149149
case TriangleFan:
150150
case TriangleStrip:
151-
case Patch:
152151
hasTriangles = true;
153152
break;
154153

154+
case Patch:
155+
logger.log(Level.SEVERE, "Tangent generation does not support mesh mode={0}", mode);
156+
return false;
157+
155158
default:
156159
logger.log(Level.SEVERE, "Tangent generation isn't implemented for mode={0}", mode);
157160
return false;
@@ -614,6 +617,7 @@ static void generateSharedVerticesIndexListSlow(int piTriList_in_and_out[], fina
614617

615618
if (vP.equals(vP2) && vN.equals(vN2) && vT.equals(vT2)) {
616619
bFound = true;
620+
index2rec = index2;
617621
} else {
618622
++j;
619623
}
@@ -662,6 +666,7 @@ static int generateInitialVerticesIndexList(TriInfo pTriInfos[], int piTriList_o
662666
//Note, Nehon: we should never get there with JME, because we don't support quads...
663667
//but I'm going to let it there in case someone needs it... Just know this code is not tested.
664668
{//TODO remove those useless brackets...
669+
pTriInfos[iDstTriIndex + 1] = new TriInfo();
665670
pTriInfos[iDstTriIndex + 1].orgFaceNumber = f;
666671
pTriInfos[iDstTriIndex + 1].tSpacesOffs = iTSpacesOffs;
667672
}
@@ -1368,6 +1373,9 @@ static void buildNeighborsFast(TriInfo pTriInfos[], Edge[] pEdges, final int piT
13681373
quickSortEdges(pEdges, iL, iR, 1, uSeed); // sort channel 1 which is i1
13691374
}
13701375
}
1376+
if (iEntries > 0) {
1377+
quickSortEdges(pEdges, iCurStartIndex, iEntries - 1, 1, uSeed);
1378+
}
13711379

13721380
// sub sort over f, which should be fast.
13731381
// this step is to remain compliant with BuildNeighborsSlow() when
@@ -1382,6 +1390,9 @@ static void buildNeighborsFast(TriInfo pTriInfos[], Edge[] pEdges, final int piT
13821390
quickSortEdges(pEdges, iL, iR, 2, uSeed); // sort channel 2 which is f
13831391
}
13841392
}
1393+
if (iEntries > 0) {
1394+
quickSortEdges(pEdges, iCurStartIndex, iEntries - 1, 2, uSeed);
1395+
}
13851396

13861397
// pair up, adjacent triangles
13871398
for (int i = 0; i < iEntries; i++) {

jme3-core/src/main/resources/Common/IBL/IBLKernels.frag

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ uniform int m_FaceId;
1616

1717
void brdfKernel(){
1818
float NdotV=TexCoords.x;
19-
float m_Roughness=TexCoords.y;
19+
float roughness=TexCoords.y;
20+
float alpha = roughness * roughness;
2021

2122
vec3 V;
2223
V.x = sqrt(1.0 - NdotV*NdotV);
@@ -28,13 +29,13 @@ void brdfKernel(){
2829
const uint SAMPLE_COUNT = 1024u;
2930
for(uint i = 0u; i < SAMPLE_COUNT; i++){
3031
vec4 Xi = Hammersley(i, SAMPLE_COUNT);
31-
vec3 H = ImportanceSampleGGX(Xi, m_Roughness, N);
32+
vec3 H = ImportanceSampleGGX(Xi, alpha, N);
3233
vec3 L = normalize(2.0 * dot(V, H) * H - V);
3334
float NdotL = max(L.z, 0.0);
3435
float NdotH = max(H.z, 0.0);
3536
float VdotH = max(dot(V, H), 0.0);
3637
if(NdotL > 0.0){
37-
float G = GeometrySmith(N, V, L, m_Roughness*m_Roughness);
38+
float G = GeometrySmith(N, V, L, alpha);
3839
float G_Vis = (G * VdotH) / (NdotH * NdotV);
3940
float Fc = pow(1.0 - VdotH, 5.0);
4041
A += (1.0 - Fc) * G_Vis;
@@ -75,26 +76,30 @@ void prefilteredEnvKernel(){
7576
vec3 R = N;
7677
vec3 V = R;
7778

78-
float a2 = m_Roughness * m_Roughness;
79+
float roughness = clamp(m_Roughness, 0.0, 1.0);
80+
float alpha = roughness * roughness;
81+
float envMapResolution = float(textureSize(m_EnvMap, 0).x);
82+
float texelSolidAngle = (4.0 * PI) / max(6.0 * envMapResolution * envMapResolution, 1.0);
83+
float maxSourceLod = max(log2(envMapResolution), 0.0);
7984

8085
const uint SAMPLE_COUNT = 1024u;
8186
float totalWeight = 0.0;
8287
vec3 prefilteredColor = vec3(0.0);
8388
for(uint i = 0u; i < SAMPLE_COUNT; ++i) {
8489
vec4 Xi = Hammersley(i, SAMPLE_COUNT);
85-
vec3 H = ImportanceSampleGGX(Xi, a2, N);
90+
vec3 H = ImportanceSampleGGX(Xi, alpha, N);
8691
float VoH = max(dot(V, H), 0.0);
8792
vec3 L = normalize(2.0 * VoH * H - V);
8893
float NdotL = max(dot(N, L), 0.0);
8994
if(NdotL > 0.0) {
90-
vec3 sampleColor = texture(m_EnvMap, L).rgb;
91-
92-
float luminance = dot(sampleColor, vec3(0.2126, 0.7152, 0.0722));
93-
if (luminance > 64.0) { // TODO use average?
94-
sampleColor *= 64.0/luminance;
95-
}
96-
97-
// TODO: use mipmap
95+
float NdotH = max(dot(N, H), 0.0);
96+
float pdf = ImportanceSampleGGXPdf(NdotH, VoH, alpha);
97+
float sampleSolidAngle = 1.0 / max(float(SAMPLE_COUNT) * pdf, 1e-4);
98+
// Approximate each cubemap texel as equal-area and select a source mip
99+
// from the ratio between the GGX sample cone and a texel footprint.
100+
float sourceLod = roughness <= 0.0 ? 0.0 : 0.5 * log2(sampleSolidAngle / texelSolidAngle);
101+
sourceLod = clamp(sourceLod, 0.0, maxSourceLod);
102+
vec3 sampleColor = textureLod(m_EnvMap, L, sourceLod).rgb;
98103
prefilteredColor += sampleColor * NdotL;
99104
totalWeight += NdotL;
100105
}
@@ -113,4 +118,4 @@ void main(){
113118
#else
114119
brdfKernel();
115120
#endif
116-
}
121+
}

jme3-core/src/main/resources/Common/IBL/Math.glsl

Lines changed: 29 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -51,45 +51,58 @@ vec4 Hammersley(uint i, uint N){
5151
// }
5252

5353

54-
vec3 ImportanceSampleGGX(vec4 Xi, float a2, vec3 N){
55-
56-
float cosTheta = sqrt((1.0 - Xi.y) / (1.0 + (a2 - 1.0) * Xi.y));
54+
// Shared roughness convention for the IBL bake path:
55+
// roughness = perceptual roughness in [0, 1]
56+
// alpha = roughness * roughness
57+
// alpha2 = alpha * alpha
58+
//
59+
// ImportanceSampleGGX() and GeometrySmith() both expect alpha.
60+
vec3 ImportanceSampleGGX(vec4 Xi, float alpha, vec3 N){
61+
float alpha2 = alpha * alpha;
62+
float cosTheta = sqrt((1.0 - Xi.y) / (1.0 + (alpha2 - 1.0) * Xi.y));
5763
float sinTheta = sqrt(1.0 - cosTheta*cosTheta);
58-
64+
5965
// from spherical coordinates to cartesian coordinates
6066
vec3 H;
6167
H.x = Xi.z * sinTheta;
6268
H.y = Xi.w * sinTheta;
6369
H.z = cosTheta;
64-
70+
6571
// from tangent-space vector to world-space sample vector
6672
vec3 up = abs(N.z) < 0.999 ? vec3(0.0, 0.0, 1.0) : vec3(1.0, 0.0, 0.0);
6773
vec3 tangent = normalize(cross(up, N));
6874
vec3 bitangent = cross(N, tangent);
69-
75+
7076
vec3 sampleVec = tangent * H.x + bitangent * H.y + N * H.z;
7177
return normalize(sampleVec);
72-
}
73-
78+
}
7479

80+
float DistributionGGX(float NdotH, float alpha) {
81+
float alpha2 = alpha * alpha;
82+
float denom = (NdotH * NdotH) * (alpha2 - 1.0) + 1.0;
83+
return alpha2 / max(PI * denom * denom, 1e-4);
84+
}
7585

86+
float ImportanceSampleGGXPdf(float NdotH, float VdotH, float alpha) {
87+
float D = DistributionGGX(NdotH, alpha);
88+
return max((D * NdotH) / max(4.0 * VdotH, 1e-4), 0.0);
89+
}
7690

77-
float GeometrySchlickGGX(float NdotV, float roughness){
78-
float a = roughness;
79-
float k = (a * a) / 2.0;
91+
float GeometrySchlickGGX(float NdotV, float alpha){
92+
float k = alpha / 2.0;
8093

8194
float nom = NdotV;
8295
float denom = NdotV * (1.0 - k) + k;
8396

8497
return nom / denom;
8598
}
8699
// ----------------------------------------------------------------------------
87-
float GeometrySmith(vec3 N, vec3 V, vec3 L, float roughness){
100+
float GeometrySmith(vec3 N, vec3 V, vec3 L, float alpha){
88101
float NdotV = max(dot(N, V), 0.0);
89102
float NdotL = max(dot(N, L), 0.0);
90-
float ggx2 = GeometrySchlickGGX(NdotV, roughness);
91-
float ggx1 = GeometrySchlickGGX(NdotL, roughness);
103+
float ggx2 = GeometrySchlickGGX(NdotV, alpha);
104+
float ggx1 = GeometrySchlickGGX(NdotL, alpha);
92105

93106
return ggx1 * ggx2;
94-
}
95-
107+
}
108+

jme3-core/src/main/resources/Common/ShaderLib/PBR.glsllib

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -61,21 +61,20 @@ float Inner_PBR_ComputeDirectLight(
6161
float ndoth = max( dot(normal, halfVec), 0.0);
6262
float hdotv = max( dot(viewDir, halfVec), 0.0);
6363

64-
// Compute diffuse using energy-conserving Lambert.
65-
// Alternatively, use Oren-Nayar for really rough
66-
// materials or if you have lots of processing power ...
67-
outDiffuse = vec3(ndotl) * lightColor;
68-
6964
//cook-torrence, microfacet BRDF : http://blog.selfshadow.com/publications/s2013-shading-course/karis/s2013_pbs_epic_notes_v2.pdf
7065

7166
//D, GGX normal Distribution function
7267
float alpha2 = alpha * alpha;
7368
float sum = ((ndoth * ndoth) * (alpha2 - 1.0) + 1.0);
7469
float denom = PI * sum * sum;
75-
float D = alpha2 / denom;
70+
float D = alpha2 / max(denom, 1e-4);
7671

7772
// Compute Fresnel function via Schlick's approximation.
7873
vec3 fresnel = F_Shlick(hdotv, fZero);
74+
// Lambert diffuse BRDF with a Fresnel-based energy split.
75+
// The caller multiplies this term by diffuseColor, which already encodes
76+
// the metallic workflow's (1 - metallic) factor.
77+
outDiffuse = vec3(ndotl / PI) * lightColor * (vec3(1.0) - fresnel);
7978

8079
//G Schlick GGX Geometry shadowing term, k = alpha/2
8180
float k = alpha * 0.5;
@@ -136,22 +135,32 @@ vec3 integrateBRDFApprox( const in vec3 specular, float roughness, float NoV ){
136135
return specular * AB.x + AB.y;
137136
}
138137

138+
float computeSpecularAO(float ao, float roughness, float NoV) {
139+
float exponent = exp2(-16.0 * roughness - 1.0);
140+
return clamp(pow(NoV + ao, exponent) - 1.0 + ao, 0.0, 1.0);
141+
}
142+
139143
// from Sebastien Lagarde https://seblagarde.files.wordpress.com/2015/07/course_notes_moving_frostbite_to_pbr_v32.pdf page 69
140-
vec3 getSpecularDominantDir(const in vec3 N, const in vec3 R, const in float realRoughness){
144+
// The Frostbite note's "real roughness" here is the microfacet alpha term,
145+
// not perceptual roughness. Callers should pass alpha = roughness * roughness.
146+
vec3 getSpecularDominantDir(const in vec3 N, const in vec3 R, const in float alpha){
141147
vec3 dominant;
142148

143-
float smoothness = 1.0 - realRoughness;
144-
float lerpFactor = smoothness * (sqrt(smoothness) + realRoughness);
149+
float smoothness = 1.0 - alpha;
150+
float lerpFactor = smoothness * (sqrt(smoothness) + alpha);
145151
// The result is not normalized as we fetch in a cubemap
146152
dominant = mix(N, R, lerpFactor);
147153

148154
return dominant;
149155
}
150156

151157
vec3 ApproximateSpecularIBL(samplerCube envMap,sampler2D integrateBRDF, vec3 SpecularColor , float Roughness, float ndotv, vec3 refVec, float nbMipMaps){
158+
// The specular bake stores roughness quadratically across mip levels:
159+
// mipNorm = mip / (nbMipMaps - 1), roughness = mipNorm * mipNorm.
160+
// Runtime therefore samples with sqrt(roughness) to recover mipNorm.
152161
float Lod = sqrt( Roughness ) * (nbMipMaps - 1.0);
153162
vec3 PrefilteredColor = textureCubeLod(envMap, refVec.xyz,Lod).rgb;
154-
vec2 EnvBRDF = texture2D(integrateBRDF,vec2(Roughness, ndotv)).rg;
163+
vec2 EnvBRDF = texture2D(integrateBRDF,vec2(ndotv, Roughness)).rg;
155164
return PrefilteredColor * ( SpecularColor * EnvBRDF.x+ EnvBRDF.y );
156165
}
157166

@@ -242,13 +251,12 @@ float renderProbe(vec3 viewDir, vec3 worldPos, vec3 normal, vec3 norm, float Rou
242251
indirectSpecular *= vec3(horiz);
243252
#endif
244253

245-
vec3 indirectLighting = (indirectDiffuse + indirectSpecular) * ao;
254+
float diffuseAO = clamp(ao.r, 0.0, 1.0);
255+
float specularAO = computeSpecularAO(diffuseAO, Roughness, ndotv);
256+
vec3 indirectLighting = indirectDiffuse * diffuseAO + indirectSpecular * specularAO;
246257

247258
color = indirectLighting * step( 0.0, probePos.w);
248259
return ndf;
249260
}
250261

251262

252-
253-
254-

0 commit comments

Comments
 (0)