Skip to content

Commit

Permalink
Add TIFF alpha channel (#1661)
Browse files Browse the repository at this point in the history
* 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 <[email protected]> (+1 squashed commits)
  • Loading branch information
Maximilian Stiede authored Nov 3, 2023
1 parent 6f99ce9 commit 50f6444
Show file tree
Hide file tree
Showing 13 changed files with 260 additions and 131 deletions.
2 changes: 1 addition & 1 deletion chunky/src/java/se/llbit/chunky/main/Chunky.java
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,7 @@ public void update() {
System.err.println("Failed to load the dump file found for this scene");
return 1;
}
PictureExportFormat outputMode = scene.getOutputMode();
PictureExportFormat outputMode = scene.getPictureExportFormat();
if (options.imageOutputFile.isEmpty()) {
options.imageOutputFile = String
.format("%s-%d%s", scene.name(), scene.spp, outputMode.getExtension());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,6 @@ public String getExtension() {
return ".pfm";
}

@Override
public boolean isTransparencySupported() {
return false;
}

@Override
public void write(OutputStream out, Scene scene, TaskTracker taskTracker) throws IOException {
try (TaskTracker.Task task = taskTracker.task("Writing PFM rows", scene.canvasHeight());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@

import java.io.IOException;
import java.io.OutputStream;

import se.llbit.chunky.renderer.scene.AlphaBuffer;
import se.llbit.chunky.renderer.scene.Scene;
import se.llbit.util.TaskTracker;

Expand Down Expand Up @@ -50,14 +52,31 @@ default String getDescription() {
String getExtension();

/**
* Check if this format supports transparency (used for transparent sky).
*
* @return True if this format supports transparency, false otherwise
* @return true, if this export format supports exporting the alpha channel
* @deprecated Replaced by {@link #getTransparencyType()} and usage of {@link AlphaBuffer}
*/
@Deprecated(forRemoval = true)
default boolean isTransparencySupported() {
return false;
}

/**
* Note: It depends on the scene settings if the alpha buffer will be available on export.
*
* @return the required format for the alpha buffer or {@link AlphaBuffer.Type#UNSUPPORTED} if alpha is not supported.
*/
default AlphaBuffer.Type getTransparencyType() {
return isTransparencySupported() ? AlphaBuffer.Type.UINT8 : AlphaBuffer.Type.UNSUPPORTED;
}

/**
* @return true, if the export formats wants preprocessed buffer data<br>
* false, if the export format uses the unprocessed sampling data
*/
default boolean wantsPostprocessing() {
return true;
}

/**
* Write the picture of the given scene into the given output stream, optionally reporting
* progress to a task tracker.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import java.io.IOException;
import java.io.OutputStream;
import se.llbit.chunky.renderer.projection.ProjectionMode;
import se.llbit.chunky.renderer.scene.AlphaBuffer;
import se.llbit.chunky.renderer.scene.Scene;
import se.llbit.chunky.resources.BitmapImage;
import se.llbit.imageformats.png.ITXT;
Expand All @@ -42,18 +43,18 @@ public String getExtension() {
}

@Override
public boolean isTransparencySupported() {
return true;
public AlphaBuffer.Type getTransparencyType() {
return AlphaBuffer.Type.UINT8;
}

@Override
public void write(OutputStream out, Scene scene, TaskTracker taskTracker) throws IOException {
try (TaskTracker.Task task = taskTracker.task("Writing PNG");
PngFileWriter writer = new PngFileWriter(out)) {
BitmapImage backBuffer = scene.getBackBuffer();
if (scene.transparentSky()) {
writer.write(backBuffer.data, scene.getAlphaChannel(), scene.canvasWidth(),
scene.canvasHeight(), task);
AlphaBuffer alpha = scene.getAlphaBuffer();
if (alpha.getType() == getTransparencyType()) {
writer.write(backBuffer.data, alpha.getBuffer(), scene.canvasWidth(), scene.canvasHeight(), task);
} else {
writer.write(backBuffer.data, scene.canvasWidth(), scene.canvasHeight(), task);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;

import se.llbit.chunky.renderer.scene.AlphaBuffer;
import se.llbit.chunky.renderer.scene.Scene;
import se.llbit.imageformats.tiff.CompressionType;
import se.llbit.imageformats.tiff.TiffFileWriter;
Expand All @@ -53,7 +54,12 @@ public String getExtension() {
}

@Override
public boolean isTransparencySupported() {
public AlphaBuffer.Type getTransparencyType() {
return AlphaBuffer.Type.FP32;
}

@Override
public boolean wantsPostprocessing() {
return false;
}

Expand Down
129 changes: 129 additions & 0 deletions chunky/src/java/se/llbit/chunky/renderer/scene/AlphaBuffer.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package se.llbit.chunky.renderer.scene;

import se.llbit.chunky.main.Chunky;
import se.llbit.chunky.renderer.WorkerState;
import se.llbit.chunky.renderer.projection.ParallelProjector;
import se.llbit.chunky.renderer.projection.ProjectionMode;
import se.llbit.log.Log;
import se.llbit.math.Ray;
import se.llbit.util.TaskTracker;

import java.nio.ByteBuffer;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.IntStream;

/**
* The AlphaBuffer acts as a cache for the alpha layer and will only be calculated on demand.
*/
public class AlphaBuffer {

public enum Type {
UNSUPPORTED(0,
(buffer, index, alphaValue) -> {
}
),
UINT8(1,
(buffer, index, alphaValue) -> buffer.put(index, (byte) (255 * alphaValue + 0.5))
),
FP32(4,
(buffer, index, alphaValue) -> buffer.putFloat(index<<2, (float) alphaValue)
);

final byte byteSize;
final AlphaWriter writer;

Type(int byteSize, AlphaWriter writer) {
this.byteSize = (byte) byteSize;
this.writer = writer;
}

@FunctionalInterface
interface AlphaWriter {
/**
* @param alphaValue 1 = occluded, 0 = transparent
*/
void write(ByteBuffer buffer, int index, double alphaValue);
}
}

private Type type = Type.UNSUPPORTED;
private ByteBuffer buffer = null;

public Type getType() {
return type;
}

public ByteBuffer getBuffer() {
return buffer;
}

public void reset() {
type = Type.UNSUPPORTED;
buffer = null;
}

/**
* Compute the alpha channel.
*/
void computeAlpha(Scene scene, Type type, TaskTracker taskTracker) {
if(type == Type.UNSUPPORTED) return;
if(this.type == type && buffer != null) return;

try (TaskTracker.Task task = taskTracker.task("Computing alpha channel")) {
this.type = type;
buffer = ByteBuffer.allocate(scene.width * scene.height * type.byteSize);

AtomicInteger done = new AtomicInteger(0);
Chunky.getCommonThreads().submit(() -> {
IntStream.range(0, scene.width).parallel().forEach(x -> {
WorkerState state = new WorkerState();
state.ray = new Ray();

for (int y = 0; y < scene.height; y++) {
computeAlpha(scene, x, y, state);
}

task.update(scene.width, done.incrementAndGet());
});
}).get();
} catch (InterruptedException | ExecutionException e) {
Log.error("Failed to compute alpha channel", e);
}
}

/**
* Compute the alpha channel based on sky visibility.
*/
public void computeAlpha(Scene scene, int x, int y, WorkerState state) {
Ray ray = state.ray;
double halfWidth = scene.width / (2.0 * scene.height);
double invHeight = 1.0 / scene.height;

// Rotated grid supersampling.
double[][] offsets = new double[][]{
{-3.0 / 8.0, 1.0 / 8.0},
{1.0 / 8.0, 3.0 / 8.0},
{-1.0 / 8.0, -3.0 / 8.0},
{3.0 / 8.0, -1.0 / 8.0},
};

double occlusion = 0.0;
for (double[] offset : offsets) {
scene.camera.calcViewRay(ray,
-halfWidth + (x + offset[0]) * invHeight,
-0.5 + (y + offset[1]) * invHeight);
ray.o.x -= scene.origin.x;
ray.o.y -= scene.origin.y;
ray.o.z -= scene.origin.z;

if (scene.camera.getProjectionMode() == ProjectionMode.PARALLEL) {
ParallelProjector.fixRay(state.ray, scene);
}
occlusion += PreviewRayTracer.skyOcclusion(scene, state);
}
occlusion /= 4.0;

type.writer.write(buffer, y * scene.width + x, occlusion);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ public class PreviewRayTracer implements RayTracer {

/**
* Calculate sky occlusion.
* @return occlusion value
* @return occlusion value (1 = occluded, 0 = transparent)
*/
public static double skyOcclusion(Scene scene, WorkerState state) {
Ray ray = state.ray;
Expand All @@ -83,7 +83,7 @@ public static double skyOcclusion(Scene scene, WorkerState state) {

/**
* Find next ray intersection.
* @return Next intersection
* @return true if intersected, false if no intersection has been found
*/
public static boolean nextIntersection(Scene scene, Ray ray) {
ray.setPrevMaterial(ray.getCurrentMaterial(), ray.getCurrentData());
Expand Down
Loading

0 comments on commit 50f6444

Please sign in to comment.