Skip to content

Commit 50f6444

Browse files
author
Maximilian Stiede
authored
Add TIFF alpha channel (#1661)
* refactor alpha channel * add alpha export to TIFF format * rename AlphaBufferType.DISABLED to UNSUPPORTED * re-add PictureExportFormat::isTransparencySupported and Scene::getAlphaChannel and flagged them as deprecated * apply feedback Co-authored-by: Maik Marschner <m.marschner@wertarbyte.com> (+1 squashed commits)
1 parent 6f99ce9 commit 50f6444

13 files changed

Lines changed: 260 additions & 131 deletions

File tree

chunky/src/java/se/llbit/chunky/main/Chunky.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -320,7 +320,7 @@ public void update() {
320320
System.err.println("Failed to load the dump file found for this scene");
321321
return 1;
322322
}
323-
PictureExportFormat outputMode = scene.getOutputMode();
323+
PictureExportFormat outputMode = scene.getPictureExportFormat();
324324
if (options.imageOutputFile.isEmpty()) {
325325
options.imageOutputFile = String
326326
.format("%s-%d%s", scene.name(), scene.spp, outputMode.getExtension());

chunky/src/java/se/llbit/chunky/renderer/export/PfmExportFormat.java

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,6 @@ public String getExtension() {
4343
return ".pfm";
4444
}
4545

46-
@Override
47-
public boolean isTransparencySupported() {
48-
return false;
49-
}
50-
5146
@Override
5247
public void write(OutputStream out, Scene scene, TaskTracker taskTracker) throws IOException {
5348
try (TaskTracker.Task task = taskTracker.task("Writing PFM rows", scene.canvasHeight());

chunky/src/java/se/llbit/chunky/renderer/export/PictureExportFormat.java

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818

1919
import java.io.IOException;
2020
import java.io.OutputStream;
21+
22+
import se.llbit.chunky.renderer.scene.AlphaBuffer;
2123
import se.llbit.chunky.renderer.scene.Scene;
2224
import se.llbit.util.TaskTracker;
2325

@@ -50,14 +52,31 @@ default String getDescription() {
5052
String getExtension();
5153

5254
/**
53-
* Check if this format supports transparency (used for transparent sky).
54-
*
55-
* @return True if this format supports transparency, false otherwise
55+
* @return true, if this export format supports exporting the alpha channel
56+
* @deprecated Replaced by {@link #getTransparencyType()} and usage of {@link AlphaBuffer}
5657
*/
58+
@Deprecated(forRemoval = true)
5759
default boolean isTransparencySupported() {
5860
return false;
5961
}
6062

63+
/**
64+
* Note: It depends on the scene settings if the alpha buffer will be available on export.
65+
*
66+
* @return the required format for the alpha buffer or {@link AlphaBuffer.Type#UNSUPPORTED} if alpha is not supported.
67+
*/
68+
default AlphaBuffer.Type getTransparencyType() {
69+
return isTransparencySupported() ? AlphaBuffer.Type.UINT8 : AlphaBuffer.Type.UNSUPPORTED;
70+
}
71+
72+
/**
73+
* @return true, if the export formats wants preprocessed buffer data<br>
74+
* false, if the export format uses the unprocessed sampling data
75+
*/
76+
default boolean wantsPostprocessing() {
77+
return true;
78+
}
79+
6180
/**
6281
* Write the picture of the given scene into the given output stream, optionally reporting
6382
* progress to a task tracker.

chunky/src/java/se/llbit/chunky/renderer/export/PngExportFormat.java

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import java.io.IOException;
2121
import java.io.OutputStream;
2222
import se.llbit.chunky.renderer.projection.ProjectionMode;
23+
import se.llbit.chunky.renderer.scene.AlphaBuffer;
2324
import se.llbit.chunky.renderer.scene.Scene;
2425
import se.llbit.chunky.resources.BitmapImage;
2526
import se.llbit.imageformats.png.ITXT;
@@ -42,18 +43,18 @@ public String getExtension() {
4243
}
4344

4445
@Override
45-
public boolean isTransparencySupported() {
46-
return true;
46+
public AlphaBuffer.Type getTransparencyType() {
47+
return AlphaBuffer.Type.UINT8;
4748
}
4849

4950
@Override
5051
public void write(OutputStream out, Scene scene, TaskTracker taskTracker) throws IOException {
5152
try (TaskTracker.Task task = taskTracker.task("Writing PNG");
5253
PngFileWriter writer = new PngFileWriter(out)) {
5354
BitmapImage backBuffer = scene.getBackBuffer();
54-
if (scene.transparentSky()) {
55-
writer.write(backBuffer.data, scene.getAlphaChannel(), scene.canvasWidth(),
56-
scene.canvasHeight(), task);
55+
AlphaBuffer alpha = scene.getAlphaBuffer();
56+
if (alpha.getType() == getTransparencyType()) {
57+
writer.write(backBuffer.data, alpha.getBuffer(), scene.canvasWidth(), scene.canvasHeight(), task);
5758
} else {
5859
writer.write(backBuffer.data, scene.canvasWidth(), scene.canvasHeight(), task);
5960
}

chunky/src/java/se/llbit/chunky/renderer/export/Tiff32ExportFormat.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import java.nio.file.Path;
2828
import java.nio.file.StandardOpenOption;
2929

30+
import se.llbit.chunky.renderer.scene.AlphaBuffer;
3031
import se.llbit.chunky.renderer.scene.Scene;
3132
import se.llbit.imageformats.tiff.CompressionType;
3233
import se.llbit.imageformats.tiff.TiffFileWriter;
@@ -53,7 +54,12 @@ public String getExtension() {
5354
}
5455

5556
@Override
56-
public boolean isTransparencySupported() {
57+
public AlphaBuffer.Type getTransparencyType() {
58+
return AlphaBuffer.Type.FP32;
59+
}
60+
61+
@Override
62+
public boolean wantsPostprocessing() {
5763
return false;
5864
}
5965

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
package se.llbit.chunky.renderer.scene;
2+
3+
import se.llbit.chunky.main.Chunky;
4+
import se.llbit.chunky.renderer.WorkerState;
5+
import se.llbit.chunky.renderer.projection.ParallelProjector;
6+
import se.llbit.chunky.renderer.projection.ProjectionMode;
7+
import se.llbit.log.Log;
8+
import se.llbit.math.Ray;
9+
import se.llbit.util.TaskTracker;
10+
11+
import java.nio.ByteBuffer;
12+
import java.util.concurrent.ExecutionException;
13+
import java.util.concurrent.atomic.AtomicInteger;
14+
import java.util.stream.IntStream;
15+
16+
/**
17+
* The AlphaBuffer acts as a cache for the alpha layer and will only be calculated on demand.
18+
*/
19+
public class AlphaBuffer {
20+
21+
public enum Type {
22+
UNSUPPORTED(0,
23+
(buffer, index, alphaValue) -> {
24+
}
25+
),
26+
UINT8(1,
27+
(buffer, index, alphaValue) -> buffer.put(index, (byte) (255 * alphaValue + 0.5))
28+
),
29+
FP32(4,
30+
(buffer, index, alphaValue) -> buffer.putFloat(index<<2, (float) alphaValue)
31+
);
32+
33+
final byte byteSize;
34+
final AlphaWriter writer;
35+
36+
Type(int byteSize, AlphaWriter writer) {
37+
this.byteSize = (byte) byteSize;
38+
this.writer = writer;
39+
}
40+
41+
@FunctionalInterface
42+
interface AlphaWriter {
43+
/**
44+
* @param alphaValue 1 = occluded, 0 = transparent
45+
*/
46+
void write(ByteBuffer buffer, int index, double alphaValue);
47+
}
48+
}
49+
50+
private Type type = Type.UNSUPPORTED;
51+
private ByteBuffer buffer = null;
52+
53+
public Type getType() {
54+
return type;
55+
}
56+
57+
public ByteBuffer getBuffer() {
58+
return buffer;
59+
}
60+
61+
public void reset() {
62+
type = Type.UNSUPPORTED;
63+
buffer = null;
64+
}
65+
66+
/**
67+
* Compute the alpha channel.
68+
*/
69+
void computeAlpha(Scene scene, Type type, TaskTracker taskTracker) {
70+
if(type == Type.UNSUPPORTED) return;
71+
if(this.type == type && buffer != null) return;
72+
73+
try (TaskTracker.Task task = taskTracker.task("Computing alpha channel")) {
74+
this.type = type;
75+
buffer = ByteBuffer.allocate(scene.width * scene.height * type.byteSize);
76+
77+
AtomicInteger done = new AtomicInteger(0);
78+
Chunky.getCommonThreads().submit(() -> {
79+
IntStream.range(0, scene.width).parallel().forEach(x -> {
80+
WorkerState state = new WorkerState();
81+
state.ray = new Ray();
82+
83+
for (int y = 0; y < scene.height; y++) {
84+
computeAlpha(scene, x, y, state);
85+
}
86+
87+
task.update(scene.width, done.incrementAndGet());
88+
});
89+
}).get();
90+
} catch (InterruptedException | ExecutionException e) {
91+
Log.error("Failed to compute alpha channel", e);
92+
}
93+
}
94+
95+
/**
96+
* Compute the alpha channel based on sky visibility.
97+
*/
98+
public void computeAlpha(Scene scene, int x, int y, WorkerState state) {
99+
Ray ray = state.ray;
100+
double halfWidth = scene.width / (2.0 * scene.height);
101+
double invHeight = 1.0 / scene.height;
102+
103+
// Rotated grid supersampling.
104+
double[][] offsets = new double[][]{
105+
{-3.0 / 8.0, 1.0 / 8.0},
106+
{1.0 / 8.0, 3.0 / 8.0},
107+
{-1.0 / 8.0, -3.0 / 8.0},
108+
{3.0 / 8.0, -1.0 / 8.0},
109+
};
110+
111+
double occlusion = 0.0;
112+
for (double[] offset : offsets) {
113+
scene.camera.calcViewRay(ray,
114+
-halfWidth + (x + offset[0]) * invHeight,
115+
-0.5 + (y + offset[1]) * invHeight);
116+
ray.o.x -= scene.origin.x;
117+
ray.o.y -= scene.origin.y;
118+
ray.o.z -= scene.origin.z;
119+
120+
if (scene.camera.getProjectionMode() == ProjectionMode.PARALLEL) {
121+
ParallelProjector.fixRay(state.ray, scene);
122+
}
123+
occlusion += PreviewRayTracer.skyOcclusion(scene, state);
124+
}
125+
occlusion /= 4.0;
126+
127+
type.writer.write(buffer, y * scene.width + x, occlusion);
128+
}
129+
}

chunky/src/java/se/llbit/chunky/renderer/scene/PreviewRayTracer.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ public class PreviewRayTracer implements RayTracer {
6262

6363
/**
6464
* Calculate sky occlusion.
65-
* @return occlusion value
65+
* @return occlusion value (1 = occluded, 0 = transparent)
6666
*/
6767
public static double skyOcclusion(Scene scene, WorkerState state) {
6868
Ray ray = state.ray;
@@ -83,7 +83,7 @@ public static double skyOcclusion(Scene scene, WorkerState state) {
8383

8484
/**
8585
* Find next ray intersection.
86-
* @return Next intersection
86+
* @return true if intersected, false if no intersection has been found
8787
*/
8888
public static boolean nextIntersection(Scene scene, Ray ray) {
8989
ray.setPrevMaterial(ray.getCurrentMaterial(), ray.getCurrentData());

0 commit comments

Comments
 (0)