Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft: Add support for PBR maps (specular, normal, metalness, roughness, emittance) #1276

Draft
wants to merge 29 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
aa1e9de
Load emission maps from PBR resourcepacks.
leMaik Dec 26, 2020
7b88023
Fix emittance value being ignored.
leMaik Dec 26, 2020
deabcc7
Refactor the emission map loading.
leMaik Dec 26, 2020
28a249f
Multiply the brightness of an emitting block by the emittance value.
leMaik Jan 3, 2021
08a5c40
Only get emittance if the block emits light.
leMaik Jan 3, 2021
4d2da69
Configure emittance maps using a system property.
leMaik Jan 3, 2021
f0a1bef
Reset the textures before loading them to get predictable results reg…
leMaik Jan 3, 2021
b68997c
Fix out of bounds error when getting the emittance.
leMaik Jan 4, 2021
aef06ee
Add relectance maps.
leMaik Jan 4, 2021
68b426c
Add roughness maps, refactor specular texture loading.
leMaik Jan 10, 2021
e15ff82
Use the roughness map to determine the roughness.
leMaik Jan 10, 2021
38d4a12
Add metalness maps.
leMaik Jan 25, 2021
8b652db
Set the emittance of the empty emission map to zero.
leMaik Jan 25, 2021
b32d064
Fix labpbr default reflectance.
leMaik Jan 25, 2021
602d819
Add a system property to override all material properties at once for…
leMaik Jan 25, 2021
ae23763
Add normal maps (for the top face of some blocks).
leMaik Feb 19, 2021
dd86bab
Fix normal map orientation.
leMaik Feb 28, 2021
abd0f9e
Fix reflectance for old bsl specular maps.
leMaik Feb 28, 2021
94b7f46
Implement default normal maps.
leMaik Mar 1, 2021
54e1fb4
Add support for all specular maps to the quad model.
leMaik Apr 5, 2021
2d45979
Fix normal map support for full blocks and quad-based models.
leMaik Jan 1, 2022
f3178ed
Probably speed up quad model by only looking up pbr textures once.
ThatRedox Jan 2, 2022
37c1e20
Implement normal mapping for AABB. refactor AABB intersection logic a…
ThatRedox Jan 2, 2022
a5f9051
Use precalculated tbn matrices.
ThatRedox Jan 2, 2022
94a875a
Remove unused `getTbn` from NormalMap
ThatRedox Jan 2, 2022
c79615a
Improve normal map performance and pbr maps memory usage.
leMaik May 26, 2022
f64eaf0
Fix tbn matrices for cube blocks.
leMaik May 26, 2022
b2dca0b
Fix rebase errors.
leMaik Oct 2, 2022
bb4fc7b
Detect PBR resource packs in the chooser.
leMaik Oct 3, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions chunky/src/java/se/llbit/chunky/block/Block.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import se.llbit.chunky.model.TexturedBlockModel;
import se.llbit.chunky.renderer.scene.Scene;
import se.llbit.chunky.resources.Texture;
import se.llbit.chunky.resources.pbr.NormalMap;
import se.llbit.chunky.world.Material;
import se.llbit.json.JsonString;
import se.llbit.json.JsonValue;
Expand Down Expand Up @@ -74,7 +75,24 @@ public boolean intersect(Ray ray, Scene scene) {
if (block.intersect(ray)) {
float[] color = texture.getColor(ray.u, ray.v);
if (color[3] > Ray.EPSILON) {
if (ray.getNormal().y > 0) {
NormalMap.apply(ray, NormalMap.tbnCubeTop, texture);
} else if (ray.getNormal().y < 0) {
NormalMap.apply(ray, NormalMap.tbnCubeBottom, texture);
} else if (ray.getNormal().x > 0) {
NormalMap.apply(ray, NormalMap.tbnCubeEast, texture);
} else if (ray.getNormal().x < 0) {
NormalMap.apply(ray, NormalMap.tbnCubeWest, texture);
} else if (ray.getNormal().z > 0) {
NormalMap.apply(ray, NormalMap.tbnCubeSouth, texture);
} else if (ray.getNormal().z < 0) {
NormalMap.apply(ray, NormalMap.tbnCubeNorth, texture);
}
ray.color.set(color);
ray.emittanceValue = texture.getEmittanceAt(ray.u, ray.v);
ray.reflectanceValue = texture.getReflectanceAt(ray.u, ray.v);
ray.roughnessValue = texture.getRoughnessAt(ray.u, ray.v);
ray.metalnessValue = texture.getMetalnessAt(ray.u, ray.v);
ray.distance += ray.tNext;
ray.o.scaleAdd(ray.tNext, ray.d);
return true;
Expand Down
40 changes: 37 additions & 3 deletions chunky/src/java/se/llbit/chunky/model/AABBModel.java
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package se.llbit.chunky.model;

import org.apache.commons.math3.util.FastMath;
import se.llbit.chunky.plugin.PluginApi;
import se.llbit.chunky.renderer.scene.Scene;
import se.llbit.chunky.resources.Texture;
import se.llbit.chunky.resources.pbr.NormalMap;
import se.llbit.math.AABB;
import se.llbit.math.Matrix3;
import se.llbit.math.Ray;
import se.llbit.math.Vector3;

Expand Down Expand Up @@ -38,6 +41,15 @@ public enum UVMapping {
FLIP_V
}

protected final static Matrix3[] tbnMatrices = {
NormalMap.tbnCubeNorth,
NormalMap.tbnCubeEast,
NormalMap.tbnCubeSouth,
NormalMap.tbnCubeWest,
NormalMap.tbnCubeTop,
NormalMap.tbnCubeBottom
};

@PluginApi
public abstract AABB[] getBoxes();

Expand Down Expand Up @@ -78,67 +90,89 @@ public boolean intersect(Ray ray, Scene scene) {

boolean hit = false;
Tint tint = Tint.NONE;
Texture hitTexture = null;
int hitSide = 0;

ray.t = Double.POSITIVE_INFINITY;
for (int i = 0; i < boxes.length; ++i) {
if (boxes[i].intersect(ray)) {
Tint[] tintedFacesBox = tintedFaces != null ? tintedFaces[i] : null;
Vector3 n = ray.getNormal();

int side = -1;
if (n.y > 0) { // top
ray.v = 1 - ray.v;
if (intersectFace(ray, scene, textures[i][4],
mapping != null ? mapping[i][4] : null
)) {
tint = tintedFacesBox != null ? tintedFacesBox[4] : Tint.NONE;
hit = true;
side = 4;
}
} else if (n.y < 0) { // bottom
if (intersectFace(ray, scene, textures[i][5],
mapping != null ? mapping[i][5] : null)) {
hit = true;
tint = tintedFacesBox != null ? tintedFacesBox[5] : Tint.NONE;
side = 5;
}
} else if (n.z < 0) { // north
if (intersectFace(ray, scene, textures[i][0],
mapping != null ? mapping[i][0] : null
)) {
hit = true;
tint = tintedFacesBox != null ? tintedFacesBox[0] : Tint.NONE;
side = 0;
}
} else if (n.z > 0) { // south
if (intersectFace(ray, scene, textures[i][2],
mapping != null ? mapping[i][2] : null
)) {
hit = true;
tint = tintedFacesBox != null ? tintedFacesBox[2] : Tint.NONE;
side = 2;
}
} else if (n.x < 0) { // west
if (intersectFace(ray, scene, textures[i][3],
mapping != null ? mapping[i][3] : null)) {
hit = true;
tint = tintedFacesBox != null ? tintedFacesBox[3] : Tint.NONE;
side = 3;
}
} else if (n.x > 0) { // east
if (intersectFace(ray, scene, textures[i][1],
mapping != null ? mapping[i][1] : null)) {
hit = true;
tint = tintedFacesBox != null ? tintedFacesBox[1] : Tint.NONE;
side = 1;
}
}

if (hit) {
ray.t = ray.tNext;
hitTexture = textures[i][side];
hitSide = side;
}
}
}
if (hit) {

if (hitTexture != null) {
if (ray.getCurrentMaterial().opaque) {
ray.color.w = 1;
}

tint.tint(ray.color, ray, scene);

NormalMap.apply(ray, tbnMatrices[hitSide], hitTexture);
ray.emittanceValue = hitTexture.getEmittanceAt(ray.u, ray.v);
ray.reflectanceValue = hitTexture.getReflectanceAt(ray.u, ray.v);
ray.roughnessValue = hitTexture.getRoughnessAt(ray.u, ray.v);
ray.metalnessValue = hitTexture.getMetalnessAt(ray.u, ray.v);

ray.distance += ray.t;
ray.o.scaleAdd(ray.t, ray.d);
}
return hit;

return hitTexture != null;
}

private boolean intersectFace(Ray ray, Scene scene, Texture texture, UVMapping mapping) {
Expand Down
13 changes: 13 additions & 0 deletions chunky/src/java/se/llbit/chunky/model/QuadModel.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import se.llbit.chunky.plugin.PluginApi;
import se.llbit.chunky.renderer.scene.Scene;
import se.llbit.chunky.resources.Texture;
import se.llbit.chunky.resources.pbr.NormalMap;
import se.llbit.math.Quad;
import se.llbit.math.Ray;
import se.llbit.math.Vector3;
Expand Down Expand Up @@ -117,6 +118,9 @@ public boolean intersect(Ray ray, Scene scene) {

float[] color = null;
Tint tint = Tint.NONE;
Quad hitQuad = null;
Texture hitTexture = null;

for (int i = 0; i < quads.length; ++i) {
Quad quad = quads[i];
if (quad.intersect(ray)) {
Expand All @@ -129,6 +133,9 @@ public boolean intersect(Ray ray, Scene scene) {
ray.orientNormal(quad.n);
else
ray.setNormal(quad.n);

hitQuad = quad;
hitTexture = textures[i];
hit = true;
}
}
Expand All @@ -143,11 +150,17 @@ public boolean intersect(Ray ray, Scene scene) {
return false;
}

NormalMap.apply(ray, hitQuad, hitTexture);
ray.color.set(color);
tint.tint(ray.color, ray, scene);
ray.emittanceValue = hitTexture.getEmittanceAt(ray.u, ray.v);
ray.reflectanceValue = hitTexture.getReflectanceAt(ray.u, ray.v);
ray.roughnessValue = hitTexture.getRoughnessAt(ray.u, ray.v);
ray.metalnessValue = hitTexture.getMetalnessAt(ray.u, ray.v);
ray.distance += ray.t;
ray.o.scaleAdd(ray.t, ray.d);
}

return hit;
}

Expand Down
21 changes: 20 additions & 1 deletion chunky/src/java/se/llbit/chunky/model/TexturedBlockModel.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import se.llbit.chunky.block.minecraft.Air;
import se.llbit.chunky.resources.Texture;
import se.llbit.chunky.resources.pbr.NormalMap;
import se.llbit.math.AABB;
import se.llbit.math.QuickMath;
import se.llbit.math.Ray;
Expand Down Expand Up @@ -63,7 +64,25 @@ public static void getIntersectionColor(Ray ray) {
return;
}
getTextureCoordinates(ray);
ray.getCurrentMaterial().getColor(ray);
Texture texture = ray.getCurrentMaterial().texture;
if (ray.getNormal().y > 0) {
NormalMap.apply(ray, NormalMap.tbnCubeTop, texture);
} else if (ray.getNormal().y < 0) {
NormalMap.apply(ray, NormalMap.tbnCubeBottom, texture);
} else if (ray.getNormal().x > 0) {
NormalMap.apply(ray, NormalMap.tbnCubeEast, texture);
} else if (ray.getNormal().x < 0) {
NormalMap.apply(ray, NormalMap.tbnCubeWest, texture);
} else if (ray.getNormal().z > 0) {
NormalMap.apply(ray, NormalMap.tbnCubeSouth, texture);
} else if (ray.getNormal().z < 0) {
NormalMap.apply(ray, NormalMap.tbnCubeNorth, texture);
}
texture.getColor(ray);
ray.emittanceValue = texture.getEmittanceAt(ray.u, ray.v);
ray.reflectanceValue = texture.getReflectanceAt(ray.u, ray.v);
ray.roughnessValue = texture.getRoughnessAt(ray.u, ray.v);
ray.metalnessValue = texture.getMetalnessAt(ray.u, ray.v);
}

/**
Expand Down
17 changes: 13 additions & 4 deletions chunky/src/java/se/llbit/chunky/renderer/scene/PathTracer.java
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ public static boolean pathTrace(Scene scene, Ray ray, WorkerState state,
if (!PreviewRayTracer.nextIntersection(scene, ray)) {
if (ray.getPrevMaterial().isWater()) {
ray.color.set(0, 0, 0, 1);
ray.emittanceValue = 0;
ray.reflectanceValue = 0;
ray.roughnessValue = 0;
ray.metalnessValue = 0;
hit = true;
} else if (ray.depth == 0) {
// Direct sky hit.
Expand Down Expand Up @@ -102,7 +106,7 @@ public static boolean pathTrace(Scene scene, Ray ray, WorkerState state,
}
}

float pSpecular = currentMat.specular;
float pSpecular = (float) (currentMat.specular * ray.reflectanceValue);

double pDiffuse = scene.fancierTranslucency ? 1 - Math.pow(1 - ray.color.w, Math.max(ray.color.x, Math.max(ray.color.y, ray.color.z))) : ray.color.w;
double pAbsorb = scene.fancierTranslucency ? 1 - (1 - ray.color.w)/(1 - pDiffuse + Ray.EPSILON) : ray.color.w;
Expand All @@ -129,7 +133,7 @@ public static boolean pathTrace(Scene scene, Ray ray, WorkerState state,
ray.depth += 1;
Vector4 cumulativeColor = new Vector4(0, 0, 0, 0);
Ray next = new Ray();
float pMetal = currentMat.metalness;
float pMetal = currentMat.metalness * ray.metalnessValue;
// Reusing first rays - a simplified form of "branched path tracing" (what Blender used to call it before they implemented something fancier)
// The initial rays cast into the scene are very similar between each sample, since they are almost entirely a function of the pixel coordinates
// Because of that, casting those initial rays on every sample is redundant and can be skipped
Expand Down Expand Up @@ -168,6 +172,10 @@ public static boolean pathTrace(Scene scene, Ray ray, WorkerState state,
}
if (!hit) {
ray.color.set(0, 0, 0, 1);
ray.emittanceValue = 0;
ray.reflectanceValue = 0;
ray.roughnessValue = 0;
ray.metalnessValue = 0;
if (firstReflection) {
airDistance = ray.distance;
}
Expand Down Expand Up @@ -228,12 +236,12 @@ private static boolean doDiffuseReflection(Ray ray, Ray next, Material currentMa
Vector3 emittance = new Vector3();
Vector4 indirectEmitterColor = new Vector4(0, 0, 0, 0);

if (scene.emittersEnabled && (!scene.isPreventNormalEmitterWithSampling() || scene.getEmitterSamplingStrategy() == EmitterSamplingStrategy.NONE || ray.depth == 0) && currentMat.emittance > Ray.EPSILON) {
if (scene.emittersEnabled && (!scene.isPreventNormalEmitterWithSampling() || scene.getEmitterSamplingStrategy() == EmitterSamplingStrategy.NONE || ray.depth == 0) && currentMat.emittance * ray.emittanceValue > Ray.EPSILON) {

// Quadratic emittance mapping, so a pixel that's 50% darker will emit only 25% as much light
// This is arbitrary but gives pretty good results in most cases.
emittance = new Vector3(ray.color.x * ray.color.x, ray.color.y * ray.color.y, ray.color.z * ray.color.z);
emittance.scale(currentMat.emittance * scene.emitterIntensity);
emittance.scale(currentMat.emittance * ray.emittanceValue * scene.emitterIntensity);

hit = true;
} else if (scene.emittersEnabled && scene.emitterSamplingStrategy != EmitterSamplingStrategy.NONE && scene.getEmitterGrid() != null) {
Expand Down Expand Up @@ -512,6 +520,7 @@ private static void sampleEmitterFace(Scene scene, Ray ray, Grid.EmitterPosition
e /= Math.max(distance * distance, 1);
e *= pos.block.surfaceArea(face);
e *= emitterRay.getCurrentMaterial().emittance;
e *= emitterRay.emittanceValue;
e *= scene.emitterIntensity;
e *= scaler;

Expand Down
12 changes: 12 additions & 0 deletions chunky/src/java/se/llbit/chunky/renderer/scene/Scene.java
Original file line number Diff line number Diff line change
Expand Up @@ -701,6 +701,10 @@ private boolean worldIntersection(Ray ray) {
ray.t = r.distance;
ray.setNormal(r.getNormal());
ray.color.set(r.color);
ray.emittanceValue = r.emittanceValue;
ray.reflectanceValue = r.reflectanceValue;
ray.roughnessValue = r.roughnessValue;
ray.metalnessValue = r.metalnessValue;
ray.setPrevMaterial(r.getPrevMaterial(), r.getPrevData());
ray.setCurrentMaterial(r.getCurrentMaterial(), r.getCurrentData());
hit = true;
Expand All @@ -712,6 +716,10 @@ private boolean worldIntersection(Ray ray) {
ray.t = r.distance;
ray.setNormal(r.getNormal());
ray.color.set(r.color);
ray.emittanceValue = r.emittanceValue;
ray.reflectanceValue = r.reflectanceValue;
ray.roughnessValue = r.roughnessValue;
ray.metalnessValue = r.metalnessValue;
ray.setPrevMaterial(r.getPrevMaterial(), r.getPrevData());
ray.setCurrentMaterial(r.getCurrentMaterial(), r.getCurrentData());
hit = true;
Expand All @@ -725,6 +733,10 @@ private boolean worldIntersection(Ray ray) {
ray.t = r.distance;
ray.setNormal(r.getNormal());
ray.color.set(r.color);
ray.emittanceValue = r.emittanceValue;
ray.reflectanceValue = r.reflectanceValue;
ray.roughnessValue = r.roughnessValue;
ray.metalnessValue = r.metalnessValue;
ray.setPrevMaterial(r.getPrevMaterial(), r.getPrevData());
ray.setCurrentMaterial(r.getCurrentMaterial(), r.getCurrentData());
hit = true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,12 +64,8 @@ default boolean hasUnloaded() {
/**
* Reset everything this loader has loaded previously. For example if this loader loads biomes, this should reset
* the biomes as if this loader had never run.
* <p/>
* Some pack loaders, eg. the {@link ResourcePackTextureLoader}, don't support this and will do nothing. Resetting
* the loaded resources might take a restart of Chunky in that case.
*/
default void resetLoadedResources() {
}
void resetLoadedResources();
}

interface PackLoaderFactory {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,13 @@ public Collection<String> notLoaded() {
return Collections.unmodifiableCollection(texturesToLoad.keySet());
}

@Override
public void resetLoadedResources() {
for (TextureLoader loader : texturesToLoad.values()) {
loader.reset();
}
}

private void loadTerrainTextures(LayeredResourcePacks root, ArrayList<String> toRemove) {
Optional<InputStream> in = Optional.empty();
try {
Expand Down
Loading
Loading