Skip to content

Commit f089f63

Browse files
PBR Megafix (#2656)
* improve PBR * Update jme3-examples/src/main/java/jme3test/light/pbr/TestPBRSimple.java Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * fixes * disable tangent test * force mipmap generation * revert mipmap generation in envbaking * Revert "revert mipmap generation in envbaking" This reverts commit 4238339. * force mipmap generation * Revert "force mipmap generation" This reverts commit 8f72617. * Reapply "revert mipmap generation in envbaking" This reverts commit c39e790. * fix * update tests * fix merge conflict --------- Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
1 parent 41c6ef4 commit f089f63

19 files changed

Lines changed: 530 additions & 186 deletions

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,6 @@
5858
import com.jme3.texture.TextureCubeMap;
5959
import com.jme3.texture.image.ColorSpace;
6060
import com.jme3.util.BufferUtils;
61-
6261
/**
6362
* Render the environment into a cubemap
6463
*

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: 8 additions & 13 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,20 @@ 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;
7981

8082
const uint SAMPLE_COUNT = 1024u;
8183
float totalWeight = 0.0;
8284
vec3 prefilteredColor = vec3(0.0);
8385
for(uint i = 0u; i < SAMPLE_COUNT; ++i) {
8486
vec4 Xi = Hammersley(i, SAMPLE_COUNT);
85-
vec3 H = ImportanceSampleGGX(Xi, a2, N);
87+
vec3 H = ImportanceSampleGGX(Xi, alpha, N);
8688
float VoH = max(dot(V, H), 0.0);
8789
vec3 L = normalize(2.0 * VoH * H - V);
8890
float NdotL = max(dot(N, L), 0.0);
8991
if(NdotL > 0.0) {
9092
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
9893
prefilteredColor += sampleColor * NdotL;
9994
totalWeight += NdotL;
10095
}
@@ -113,4 +108,4 @@ void main(){
113108
#else
114109
brdfKernel();
115110
#endif
116-
}
111+
}

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

Lines changed: 38 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -51,45 +51,67 @@ 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+
const float MIN_GGX_ALPHA = 0.0064;
61+
62+
float SafeGGXAlpha(float alpha) {
63+
return max(alpha, MIN_GGX_ALPHA);
64+
}
65+
66+
vec3 ImportanceSampleGGX(vec4 Xi, float alpha, vec3 N){
67+
alpha = SafeGGXAlpha(alpha);
68+
float alpha2 = alpha * alpha;
69+
float cosTheta = sqrt((1.0 - Xi.y) / (1.0 + (alpha2 - 1.0) * Xi.y));
5770
float sinTheta = sqrt(1.0 - cosTheta*cosTheta);
58-
71+
5972
// from spherical coordinates to cartesian coordinates
6073
vec3 H;
6174
H.x = Xi.z * sinTheta;
6275
H.y = Xi.w * sinTheta;
6376
H.z = cosTheta;
64-
77+
6578
// from tangent-space vector to world-space sample vector
6679
vec3 up = abs(N.z) < 0.999 ? vec3(0.0, 0.0, 1.0) : vec3(1.0, 0.0, 0.0);
6780
vec3 tangent = normalize(cross(up, N));
6881
vec3 bitangent = cross(N, tangent);
69-
82+
7083
vec3 sampleVec = tangent * H.x + bitangent * H.y + N * H.z;
7184
return normalize(sampleVec);
72-
}
73-
85+
}
7486

87+
float DistributionGGX(float NdotH, float alpha) {
88+
alpha = SafeGGXAlpha(alpha);
89+
float alpha2 = alpha * alpha;
90+
float denom = (NdotH * NdotH) * (alpha2 - 1.0) + 1.0;
91+
return alpha2 / (PI * denom * denom);
92+
}
7593

94+
float ImportanceSampleGGXPdf(float NdotH, float VdotH, float alpha) {
95+
float D = DistributionGGX(NdotH, alpha);
96+
return max((D * NdotH) / max(4.0 * VdotH, 1e-4), 0.0);
97+
}
7698

77-
float GeometrySchlickGGX(float NdotV, float roughness){
78-
float a = roughness;
79-
float k = (a * a) / 2.0;
99+
float GeometrySchlickGGX(float NdotV, float alpha){
100+
alpha = SafeGGXAlpha(alpha);
101+
float k = alpha / 2.0;
80102

81103
float nom = NdotV;
82104
float denom = NdotV * (1.0 - k) + k;
83105

84106
return nom / denom;
85107
}
86108
// ----------------------------------------------------------------------------
87-
float GeometrySmith(vec3 N, vec3 V, vec3 L, float roughness){
109+
float GeometrySmith(vec3 N, vec3 V, vec3 L, float alpha){
88110
float NdotV = max(dot(N, V), 0.0);
89111
float NdotL = max(dot(N, L), 0.0);
90-
float ggx2 = GeometrySchlickGGX(NdotV, roughness);
91-
float ggx1 = GeometrySchlickGGX(NdotL, roughness);
112+
float ggx2 = GeometrySchlickGGX(NdotV, alpha);
113+
float ggx1 = GeometrySchlickGGX(NdotL, alpha);
92114

93115
return ggx1 * ggx2;
94-
}
95-
116+
}
117+

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

Lines changed: 29 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@
66
#define NB_PROBES 0
77
#endif
88

9+
// Internal GGX stability floor. This does not change the user-facing
10+
// roughness convention or probe LOD mapping; it only prevents extremely sharp
11+
// microfacet lobes from reaching pathological peaks that can destabilize some
12+
// drivers and screenshot-test software renderers.
13+
const float MIN_GGX_ALPHA = 0.0064;
14+
915
// BEGIN-@jhonkkk,Specular AA --------------------------------------------------------------
1016
// see:http://www.jp.square-enix.com/tech/library/pdf/ImprovedGeometricSpecularAA(slides).pdf
1117
// https://docs.unity3d.com/Packages/com.unity.render-pipelines.high-definition@15.0/manual/Geometric-Specular-Anti-Aliasing.html
@@ -55,27 +61,27 @@ float Inner_PBR_ComputeDirectLight(
5561
vec3 normal, vec3 halfVec, vec3 lightDir, vec3 viewDir,
5662
vec3 lightColor, vec3 fZero, float alpha, float ndotv,
5763
out vec3 outDiffuse, out vec3 outSpecular){
64+
alpha = max(alpha, MIN_GGX_ALPHA);
5865

5966
// Compute ndotl, ndoth, vdoth terms which are needed later.
6067
float ndotl = max( dot(normal, lightDir), 0.0);
6168
float ndoth = max( dot(normal, halfVec), 0.0);
6269
float hdotv = max( dot(viewDir, halfVec), 0.0);
6370

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-
6971
//cook-torrence, microfacet BRDF : http://blog.selfshadow.com/publications/s2013-shading-course/karis/s2013_pbs_epic_notes_v2.pdf
7072

7173
//D, GGX normal Distribution function
7274
float alpha2 = alpha * alpha;
7375
float sum = ((ndoth * ndoth) * (alpha2 - 1.0) + 1.0);
7476
float denom = PI * sum * sum;
75-
float D = alpha2 / denom;
77+
float D = alpha2 / denom;
7678

7779
// Compute Fresnel function via Schlick's approximation.
7880
vec3 fresnel = F_Shlick(hdotv, fZero);
81+
// Lambert diffuse BRDF with a Fresnel-based energy split.
82+
// The caller multiplies this term by diffuseColor, which already encodes
83+
// the metallic workflow's (1 - metallic) factor.
84+
outDiffuse = vec3(ndotl / PI) * lightColor * (vec3(1.0) - fresnel);
7985

8086
//G Schlick GGX Geometry shadowing term, k = alpha/2
8187
float k = alpha * 0.5;
@@ -136,22 +142,32 @@ vec3 integrateBRDFApprox( const in vec3 specular, float roughness, float NoV ){
136142
return specular * AB.x + AB.y;
137143
}
138144

145+
float computeSpecularAO(float ao, float roughness, float NoV) {
146+
float exponent = exp2(-16.0 * roughness - 1.0);
147+
return clamp(pow(NoV + ao, exponent) - 1.0 + ao, 0.0, 1.0);
148+
}
149+
139150
// 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){
151+
// The Frostbite note's "real roughness" here is the microfacet alpha term,
152+
// not perceptual roughness. Callers should pass alpha = roughness * roughness.
153+
vec3 getSpecularDominantDir(const in vec3 N, const in vec3 R, const in float alpha){
141154
vec3 dominant;
142155

143-
float smoothness = 1.0 - realRoughness;
144-
float lerpFactor = smoothness * (sqrt(smoothness) + realRoughness);
156+
float smoothness = 1.0 - alpha;
157+
float lerpFactor = smoothness * (sqrt(smoothness) + alpha);
145158
// The result is not normalized as we fetch in a cubemap
146159
dominant = mix(N, R, lerpFactor);
147160

148161
return dominant;
149162
}
150163

151164
vec3 ApproximateSpecularIBL(samplerCube envMap,sampler2D integrateBRDF, vec3 SpecularColor , float Roughness, float ndotv, vec3 refVec, float nbMipMaps){
165+
// The specular bake stores roughness quadratically across mip levels:
166+
// mipNorm = mip / (nbMipMaps - 1), roughness = mipNorm * mipNorm.
167+
// Runtime therefore samples with sqrt(roughness) to recover mipNorm.
152168
float Lod = sqrt( Roughness ) * (nbMipMaps - 1.0);
153169
vec3 PrefilteredColor = textureCubeLod(envMap, refVec.xyz,Lod).rgb;
154-
vec2 EnvBRDF = texture2D(integrateBRDF,vec2(Roughness, ndotv)).rg;
170+
vec2 EnvBRDF = texture2D(integrateBRDF,vec2(ndotv, Roughness)).rg;
155171
return PrefilteredColor * ( SpecularColor * EnvBRDF.x+ EnvBRDF.y );
156172
}
157173

@@ -242,13 +258,10 @@ float renderProbe(vec3 viewDir, vec3 worldPos, vec3 normal, vec3 norm, float Rou
242258
indirectSpecular *= vec3(horiz);
243259
#endif
244260

245-
vec3 indirectLighting = (indirectDiffuse + indirectSpecular) * ao;
261+
float diffuseAO = clamp(ao.r, 0.0, 1.0);
262+
float specularAO = computeSpecularAO(diffuseAO, Roughness, ndotv);
263+
vec3 indirectLighting = indirectDiffuse * diffuseAO + indirectSpecular * specularAO;
246264

247265
color = indirectLighting * step( 0.0, probePos.w);
248266
return ndf;
249267
}
250-
251-
252-
253-
254-

0 commit comments

Comments
 (0)