properly implement transfer functions and apply them to the output image
This commit is contained in:
parent
b8aae8c2e5
commit
533461204a
@ -1,9 +1,9 @@
|
|||||||
package eu.jonahbauer.raytracing;
|
package eu.jonahbauer.raytracing;
|
||||||
|
|
||||||
import eu.jonahbauer.raytracing.render.ImageFormat;
|
|
||||||
import eu.jonahbauer.raytracing.render.canvas.Canvas;
|
import eu.jonahbauer.raytracing.render.canvas.Canvas;
|
||||||
import eu.jonahbauer.raytracing.render.canvas.LiveCanvas;
|
import eu.jonahbauer.raytracing.render.canvas.LiveCanvas;
|
||||||
import eu.jonahbauer.raytracing.render.canvas.XYZCanvas;
|
import eu.jonahbauer.raytracing.render.canvas.XYZCanvas;
|
||||||
|
import eu.jonahbauer.raytracing.render.image.PNGImageWriter;
|
||||||
import eu.jonahbauer.raytracing.render.renderer.SimpleRenderer;
|
import eu.jonahbauer.raytracing.render.renderer.SimpleRenderer;
|
||||||
import eu.jonahbauer.raytracing.render.color.ColorSpaces;
|
import eu.jonahbauer.raytracing.render.color.ColorSpaces;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
@ -42,7 +42,7 @@ public class Main {
|
|||||||
renderer.render(camera, scene, canvas);
|
renderer.render(camera, scene, canvas);
|
||||||
System.out.printf("rendering finished after %dms", (System.nanoTime() - time) / 1_000_000);
|
System.out.printf("rendering finished after %dms", (System.nanoTime() - time) / 1_000_000);
|
||||||
|
|
||||||
ImageFormat.PNG.write(canvas, config.path);
|
PNGImageWriter.sRGB.write(canvas, config.path);
|
||||||
}
|
}
|
||||||
|
|
||||||
private record Config(@NotNull Example example, @NotNull Path path, boolean preview, boolean iterative, boolean parallel, int samples, int depth) {
|
private record Config(@NotNull Example example, @NotNull Path path, boolean preview, boolean iterative, boolean parallel, int samples, int depth) {
|
||||||
|
@ -1,135 +0,0 @@
|
|||||||
package eu.jonahbauer.raytracing.render;
|
|
||||||
|
|
||||||
import eu.jonahbauer.raytracing.render.canvas.Canvas;
|
|
||||||
import eu.jonahbauer.raytracing.render.color.ColorSpaces;
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
|
|
||||||
import java.io.*;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.util.zip.CRC32;
|
|
||||||
import java.util.zip.CheckedOutputStream;
|
|
||||||
import java.util.zip.DeflaterOutputStream;
|
|
||||||
|
|
||||||
public enum ImageFormat {
|
|
||||||
PPM {
|
|
||||||
@Override
|
|
||||||
public void write(@NotNull Canvas image, @NotNull OutputStream out) throws IOException {
|
|
||||||
try (var writer = new BufferedWriter(new OutputStreamWriter(out, StandardCharsets.US_ASCII))) {
|
|
||||||
writer.write("P3\n");
|
|
||||||
writer.write(String.valueOf(image.getWidth()));
|
|
||||||
writer.write(" ");
|
|
||||||
writer.write(String.valueOf(image.getHeight()));
|
|
||||||
writer.write("\n255\n");
|
|
||||||
|
|
||||||
for (int y = 0; y < image.getHeight(); y++) {
|
|
||||||
for (int x = 0; x < image.getWidth(); x++) {
|
|
||||||
var color = image.getRGB(x, y, ColorSpaces.sRGB);
|
|
||||||
writer.write(String.valueOf(color.red()));
|
|
||||||
writer.write(" ");
|
|
||||||
writer.write(String.valueOf(color.green()));
|
|
||||||
writer.write(" ");
|
|
||||||
writer.write(String.valueOf(color.blue()));
|
|
||||||
writer.write("\n");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
PNG {
|
|
||||||
private static final byte[] MAGIC = new byte[] { (byte) 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A };
|
|
||||||
private static final int IHDR_LENGTH = 13;
|
|
||||||
private static final int IHDR_TYPE = 0x49484452;
|
|
||||||
private static final int IDAT_TYPE = 0x49444154;
|
|
||||||
private static final int IEND_TYPE = 0x49454E44;
|
|
||||||
private static final int IEND_CRC = 0xAE426082;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void write(@NotNull Canvas image, @NotNull OutputStream out) throws IOException {
|
|
||||||
try (var data = new NoCloseDataOutputStream(out); var _ = data.closeable()) {
|
|
||||||
data.write(MAGIC);
|
|
||||||
|
|
||||||
writeIHDR(image, data);
|
|
||||||
writeIDAT(image, data);
|
|
||||||
writeIEND(image, data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void writeIHDR(@NotNull Canvas image, @NotNull DataOutputStream data) throws IOException {
|
|
||||||
data.writeInt(IHDR_LENGTH);
|
|
||||||
try (
|
|
||||||
var crc = new CheckedOutputStream(data, new CRC32());
|
|
||||||
var ihdr = new DataOutputStream(crc)
|
|
||||||
) {
|
|
||||||
ihdr.writeInt(IHDR_TYPE);
|
|
||||||
ihdr.writeInt(image.getWidth()); // image width
|
|
||||||
ihdr.writeInt(image.getHeight()); // image height
|
|
||||||
ihdr.writeByte(8); // bit depth
|
|
||||||
ihdr.writeByte(2); // color type
|
|
||||||
ihdr.writeByte(0); // compression method
|
|
||||||
ihdr.writeByte(0); // filter method
|
|
||||||
ihdr.writeByte(0); // interlace method
|
|
||||||
ihdr.flush();
|
|
||||||
data.writeInt((int) crc.getChecksum().getValue());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void writeIDAT(@NotNull Canvas image, @NotNull DataOutputStream data) throws IOException {
|
|
||||||
try (
|
|
||||||
var baos = new ByteArrayOutputStream();
|
|
||||||
var crc = new CheckedOutputStream(baos, new CRC32());
|
|
||||||
var idat = new DataOutputStream(crc)
|
|
||||||
) {
|
|
||||||
idat.writeInt(IDAT_TYPE);
|
|
||||||
|
|
||||||
try (var deflate = new DataOutputStream(new DeflaterOutputStream(idat))) {
|
|
||||||
for (int y = 0; y < image.getHeight(); y++) {
|
|
||||||
deflate.writeByte(0); // filter type
|
|
||||||
for (int x = 0; x < image.getWidth(); x++) {
|
|
||||||
var pixel = image.getRGB(x, y, ColorSpaces.sRGB);
|
|
||||||
deflate.writeByte(pixel.red());
|
|
||||||
deflate.writeByte(pixel.green());
|
|
||||||
deflate.writeByte(pixel.blue());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var bytes = baos.toByteArray();
|
|
||||||
data.writeInt(bytes.length - 4); // don't include type in length
|
|
||||||
data.write(bytes);
|
|
||||||
data.writeInt((int) crc.getChecksum().getValue());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void writeIEND(@NotNull Canvas image, @NotNull DataOutputStream data) throws IOException {
|
|
||||||
data.writeInt(0);
|
|
||||||
data.writeInt(IEND_TYPE);
|
|
||||||
data.writeInt(IEND_CRC);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class NoCloseDataOutputStream extends DataOutputStream {
|
|
||||||
public NoCloseDataOutputStream(OutputStream out) {
|
|
||||||
super(out);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void close() {
|
|
||||||
// do nothing
|
|
||||||
}
|
|
||||||
|
|
||||||
public Closeable closeable() {
|
|
||||||
return super::close;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
;
|
|
||||||
|
|
||||||
public void write(@NotNull Canvas image, @NotNull Path path) throws IOException {
|
|
||||||
try (var out = Files.newOutputStream(path)) {
|
|
||||||
write(image, out);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public abstract void write(@NotNull Canvas image, @NotNull OutputStream out) throws IOException;
|
|
||||||
}
|
|
@ -36,7 +36,7 @@ public final class LiveCanvas implements Canvas {
|
|||||||
@Override
|
@Override
|
||||||
public void add(int x, int y, int n, @NotNull SampledSpectrum spectrum, @NotNull SampledWavelengths lambda) {
|
public void add(int x, int y, int n, @NotNull SampledSpectrum spectrum, @NotNull SampledWavelengths lambda) {
|
||||||
delegate.add(x, y, n, spectrum, lambda);
|
delegate.add(x, y, n, spectrum, lambda);
|
||||||
var color = ColorRGB.gamma(delegate.getRGB(x, y, cs));
|
var color = cs.encode(delegate.getRGB(x, y, cs));
|
||||||
var rgb = color.red() << 16 | color.green() << 8 | color.blue();
|
var rgb = color.red() << 16 | color.green() << 8 | color.blue();
|
||||||
image.setRGB(x, y, rgb);
|
image.setRGB(x, y, rgb);
|
||||||
}
|
}
|
||||||
|
@ -58,24 +58,6 @@ public record ColorRGB(double r, double g, double b) implements IVec3<ColorRGB>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static @NotNull ColorRGB gamma(@NotNull ColorRGB color) {
|
|
||||||
return new ColorRGB(gamma(color.r), gamma(color.g), gamma(color.b));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static @NotNull ColorRGB inverseGamma(@NotNull ColorRGB color) {
|
|
||||||
return new ColorRGB(inverseGamma(color.r), inverseGamma(color.g), inverseGamma(color.b));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static double gamma(double value) {
|
|
||||||
if (value <= 0.0031308) return 12.92 * value;
|
|
||||||
return 1.055 * Math.pow(value, 1. / 2.4) - 0.055;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static double inverseGamma(double value) {
|
|
||||||
if (value <= 0.04045) return value / 12.92;
|
|
||||||
return Math.pow((value + 0.055) / 1.055, 2.4d);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public @NotNull ColorRGB plus(@NotNull ColorRGB other) {
|
public @NotNull ColorRGB plus(@NotNull ColorRGB other) {
|
||||||
return new ColorRGB(r + other.r, g + other.g, b + other.b);
|
return new ColorRGB(r + other.r, g + other.g, b + other.b);
|
||||||
|
@ -25,16 +25,18 @@ public final class ColorSpace {
|
|||||||
private final @NotNull Matrix3 XYZfromRGB;
|
private final @NotNull Matrix3 XYZfromRGB;
|
||||||
private final @NotNull Matrix3 RGBfromXYZ;
|
private final @NotNull Matrix3 RGBfromXYZ;
|
||||||
private final @NotNull SpectrumTable RGBtoSpectrumTable;
|
private final @NotNull SpectrumTable RGBtoSpectrumTable;
|
||||||
|
private final @NotNull TransferFunction transferFunction;
|
||||||
|
|
||||||
public ColorSpace(
|
public ColorSpace(
|
||||||
@NotNull Chromaticity r, @NotNull Chromaticity g, @NotNull Chromaticity b,
|
@NotNull Chromaticity r, @NotNull Chromaticity g, @NotNull Chromaticity b,
|
||||||
@NotNull Spectrum illuminant, @NotNull SpectrumTable table
|
@NotNull Spectrum illuminant, @NotNull SpectrumTable table, @NotNull TransferFunction transferFunction
|
||||||
) {
|
) {
|
||||||
this.r = Objects.requireNonNull(r, "r");
|
this.r = Objects.requireNonNull(r, "r");
|
||||||
this.g = Objects.requireNonNull(g, "g");
|
this.g = Objects.requireNonNull(g, "g");
|
||||||
this.b = Objects.requireNonNull(b, "b");
|
this.b = Objects.requireNonNull(b, "b");
|
||||||
this.illuminant = new DenselySampledSpectrum(illuminant);
|
this.illuminant = new DenselySampledSpectrum(illuminant);
|
||||||
this.RGBtoSpectrumTable = table;
|
this.RGBtoSpectrumTable = table; // no null-check
|
||||||
|
this.transferFunction = transferFunction; // no null-check
|
||||||
|
|
||||||
this.W = illuminant.toXYZ();
|
this.W = illuminant.toXYZ();
|
||||||
this.w = W.xy();
|
this.w = W.xy();
|
||||||
@ -53,6 +55,10 @@ public final class ColorSpace {
|
|||||||
this.RGBfromXYZ = XYZfromRGB.invert();
|
this.RGBfromXYZ = XYZfromRGB.invert();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Conversions
|
||||||
|
*/
|
||||||
|
|
||||||
public @NotNull ColorRGB toRGB(@NotNull ColorXYZ xyz) {
|
public @NotNull ColorRGB toRGB(@NotNull ColorXYZ xyz) {
|
||||||
var out = RGBfromXYZ.times(xyz.toVec3());
|
var out = RGBfromXYZ.times(xyz.toVec3());
|
||||||
return new ColorRGB(out.x(), out.y(), out.z());
|
return new ColorRGB(out.x(), out.y(), out.z());
|
||||||
@ -84,6 +90,18 @@ public final class ColorSpace {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public @NotNull ColorRGB encode(@NotNull ColorRGB rgb) {
|
||||||
|
return transferFunction.encode(rgb);
|
||||||
|
}
|
||||||
|
|
||||||
|
public @NotNull ColorRGB decode(@NotNull ColorRGB rgb) {
|
||||||
|
return transferFunction.decode(rgb);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Spectrum
|
||||||
|
*/
|
||||||
|
|
||||||
public @NotNull SigmoidPolynomial toPolynomial(@NotNull ColorRGB rgb) {
|
public @NotNull SigmoidPolynomial toPolynomial(@NotNull ColorRGB rgb) {
|
||||||
return RGBtoSpectrumTable.get(new ColorRGB(
|
return RGBtoSpectrumTable.get(new ColorRGB(
|
||||||
Math.max(0, rgb.r()),
|
Math.max(0, rgb.r()),
|
||||||
|
@ -13,21 +13,21 @@ public final class ColorSpaces {
|
|||||||
new Chromaticity(0.6400, 0.3300),
|
new Chromaticity(0.6400, 0.3300),
|
||||||
new Chromaticity(0.3000, 0.6000),
|
new Chromaticity(0.3000, 0.6000),
|
||||||
new Chromaticity(0.1500, 0.0600),
|
new Chromaticity(0.1500, 0.0600),
|
||||||
Spectra.D65, read("sRGB_spectrum.bin")
|
Spectra.D65, read("sRGB_spectrum.bin"), TransferFunctions.sRGB
|
||||||
);
|
);
|
||||||
// P3-D65 (display)
|
// P3-D65 (display)
|
||||||
public static final @NotNull ColorSpace DCI_P3 = new ColorSpace(
|
public static final @NotNull ColorSpace DCI_P3 = new ColorSpace(
|
||||||
new Chromaticity(0.680, 0.320),
|
new Chromaticity(0.680, 0.320),
|
||||||
new Chromaticity(0.265, 0.690),
|
new Chromaticity(0.265, 0.690),
|
||||||
new Chromaticity(0.150, 0.060),
|
new Chromaticity(0.150, 0.060),
|
||||||
Spectra.D65, read("DCI_P3_spectrum.bin")
|
Spectra.D65, read("DCI_P3_spectrum.bin"), TransferFunctions.sRGB
|
||||||
);
|
);
|
||||||
// ITU-R Rec BT.2020
|
// ITU-R Rec BT.2020
|
||||||
public static final @NotNull ColorSpace Rec2020 = new ColorSpace(
|
public static final @NotNull ColorSpace Rec2020 = new ColorSpace(
|
||||||
new Chromaticity(0.708, 0.292),
|
new Chromaticity(0.708, 0.292),
|
||||||
new Chromaticity(0.170, 0.797),
|
new Chromaticity(0.170, 0.797),
|
||||||
new Chromaticity(0.131, 0.046),
|
new Chromaticity(0.131, 0.046),
|
||||||
Spectra.D65, read("Rec2020_spectrum.bin")
|
Spectra.D65, read("Rec2020_spectrum.bin"), null
|
||||||
);
|
);
|
||||||
|
|
||||||
private static @NotNull SpectrumTable read(@NotNull String name) {
|
private static @NotNull SpectrumTable read(@NotNull String name) {
|
||||||
|
@ -0,0 +1,8 @@
|
|||||||
|
package eu.jonahbauer.raytracing.render.color;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
public interface TransferFunction {
|
||||||
|
@NotNull ColorRGB decode(@NotNull ColorRGB rgb);
|
||||||
|
@NotNull ColorRGB encode(@NotNull ColorRGB rgb);
|
||||||
|
}
|
@ -0,0 +1,50 @@
|
|||||||
|
package eu.jonahbauer.raytracing.render.color;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
public final class TransferFunctions {
|
||||||
|
public static final @NotNull TransferFunction sRGB = new ComponentTransferFunction() {
|
||||||
|
@Override
|
||||||
|
protected double encode(double value) {
|
||||||
|
if (value <= 0.0031308) return 12.92 * value;
|
||||||
|
return 1.055 * Math.pow(value, 1. / 2.4) - 0.055;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected double decode(double value) {
|
||||||
|
if (value <= 0.04045) return value / 12.92;
|
||||||
|
return Math.pow((value + 0.055) / 1.055, 2.4d);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public static final @NotNull TransferFunction LINEAR = new TransferFunction() {
|
||||||
|
@Override
|
||||||
|
public @NotNull ColorRGB encode(@NotNull ColorRGB rgb) {
|
||||||
|
return rgb;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NotNull ColorRGB decode(@NotNull ColorRGB rgb) {
|
||||||
|
return rgb;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private TransferFunctions() {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
private abstract static class ComponentTransferFunction implements TransferFunction {
|
||||||
|
@Override
|
||||||
|
public final @NotNull ColorRGB decode(@NotNull ColorRGB rgb) {
|
||||||
|
return new ColorRGB(decode(rgb.r()), decode(rgb.g()), decode(rgb.b()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final @NotNull ColorRGB encode(@NotNull ColorRGB rgb) {
|
||||||
|
return new ColorRGB(encode(rgb.r()), encode(rgb.g()), encode(rgb.b()));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract double encode(double value);
|
||||||
|
protected abstract double decode(double value);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,19 @@
|
|||||||
|
package eu.jonahbauer.raytracing.render.image;
|
||||||
|
|
||||||
|
import eu.jonahbauer.raytracing.render.canvas.Canvas;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
public interface ImageWriter {
|
||||||
|
default void write(@NotNull Canvas image, @NotNull Path path) throws IOException {
|
||||||
|
try (var out = Files.newOutputStream(path)) {
|
||||||
|
write(image, out);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void write(@NotNull Canvas canvas, @NotNull OutputStream out) throws IOException;
|
||||||
|
}
|
@ -0,0 +1,107 @@
|
|||||||
|
package eu.jonahbauer.raytracing.render.image;
|
||||||
|
|
||||||
|
import eu.jonahbauer.raytracing.render.canvas.Canvas;
|
||||||
|
import eu.jonahbauer.raytracing.render.color.ColorSpace;
|
||||||
|
import eu.jonahbauer.raytracing.render.color.ColorSpaces;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.zip.CRC32;
|
||||||
|
import java.util.zip.CheckedOutputStream;
|
||||||
|
import java.util.zip.DeflaterOutputStream;
|
||||||
|
|
||||||
|
public class PNGImageWriter implements ImageWriter {
|
||||||
|
public static final @NotNull PNGImageWriter sRGB = new PNGImageWriter(ColorSpaces.sRGB);
|
||||||
|
|
||||||
|
private static final byte[] MAGIC = new byte[] { (byte) 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A };
|
||||||
|
private static final int IHDR_LENGTH = 13;
|
||||||
|
private static final int IHDR_TYPE = 0x49484452;
|
||||||
|
private static final int IDAT_TYPE = 0x49444154;
|
||||||
|
private static final int IEND_TYPE = 0x49454E44;
|
||||||
|
private static final int IEND_CRC = 0xAE426082;
|
||||||
|
|
||||||
|
private final @NotNull ColorSpace cs;
|
||||||
|
|
||||||
|
public PNGImageWriter(@NotNull ColorSpace cs) {
|
||||||
|
this.cs = Objects.requireNonNull(cs, "cs");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void write(@NotNull Canvas image, @NotNull OutputStream out) throws IOException {
|
||||||
|
try (var data = new NoCloseDataOutputStream(out); var _ = data.closeable()) {
|
||||||
|
data.write(MAGIC);
|
||||||
|
|
||||||
|
writeIHDR(image, data);
|
||||||
|
writeIDAT(image, data);
|
||||||
|
writeIEND(image, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeIHDR(@NotNull Canvas image, @NotNull DataOutputStream data) throws IOException {
|
||||||
|
data.writeInt(IHDR_LENGTH);
|
||||||
|
try (
|
||||||
|
var crc = new CheckedOutputStream(data, new CRC32());
|
||||||
|
var ihdr = new DataOutputStream(crc)
|
||||||
|
) {
|
||||||
|
ihdr.writeInt(IHDR_TYPE);
|
||||||
|
ihdr.writeInt(image.getWidth()); // image width
|
||||||
|
ihdr.writeInt(image.getHeight()); // image height
|
||||||
|
ihdr.writeByte(8); // bit depth
|
||||||
|
ihdr.writeByte(2); // color type
|
||||||
|
ihdr.writeByte(0); // compression method
|
||||||
|
ihdr.writeByte(0); // filter method
|
||||||
|
ihdr.writeByte(0); // interlace method
|
||||||
|
ihdr.flush();
|
||||||
|
data.writeInt((int) crc.getChecksum().getValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeIDAT(@NotNull Canvas image, @NotNull DataOutputStream data) throws IOException {
|
||||||
|
try (
|
||||||
|
var baos = new ByteArrayOutputStream();
|
||||||
|
var crc = new CheckedOutputStream(baos, new CRC32());
|
||||||
|
var idat = new DataOutputStream(crc)
|
||||||
|
) {
|
||||||
|
idat.writeInt(IDAT_TYPE);
|
||||||
|
|
||||||
|
try (var deflate = new DataOutputStream(new DeflaterOutputStream(idat))) {
|
||||||
|
for (int y = 0; y < image.getHeight(); y++) {
|
||||||
|
deflate.writeByte(0); // filter type
|
||||||
|
for (int x = 0; x < image.getWidth(); x++) {
|
||||||
|
var pixel = cs.encode(image.getRGB(x, y, cs));
|
||||||
|
deflate.writeByte(pixel.red());
|
||||||
|
deflate.writeByte(pixel.green());
|
||||||
|
deflate.writeByte(pixel.blue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var bytes = baos.toByteArray();
|
||||||
|
data.writeInt(bytes.length - 4); // don't include type in length
|
||||||
|
data.write(bytes);
|
||||||
|
data.writeInt((int) crc.getChecksum().getValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeIEND(@NotNull Canvas image, @NotNull DataOutputStream data) throws IOException {
|
||||||
|
data.writeInt(0);
|
||||||
|
data.writeInt(IEND_TYPE);
|
||||||
|
data.writeInt(IEND_CRC);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class NoCloseDataOutputStream extends DataOutputStream {
|
||||||
|
public NoCloseDataOutputStream(OutputStream out) {
|
||||||
|
super(out);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
public Closeable closeable() {
|
||||||
|
return super::close;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,46 @@
|
|||||||
|
package eu.jonahbauer.raytracing.render.image;
|
||||||
|
|
||||||
|
import eu.jonahbauer.raytracing.render.canvas.Canvas;
|
||||||
|
import eu.jonahbauer.raytracing.render.color.ColorSpace;
|
||||||
|
import eu.jonahbauer.raytracing.render.color.ColorSpaces;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import java.io.BufferedWriter;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.io.OutputStreamWriter;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public class PPMImageWriter implements ImageWriter {
|
||||||
|
public static final PPMImageWriter sRGB = new PPMImageWriter(ColorSpaces.sRGB);
|
||||||
|
|
||||||
|
private final @NotNull ColorSpace cs;
|
||||||
|
|
||||||
|
public PPMImageWriter(@NotNull ColorSpace cs) {
|
||||||
|
this.cs = Objects.requireNonNull(cs, "cs");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void write(@NotNull Canvas image, @NotNull OutputStream out) throws IOException {
|
||||||
|
try (var writer = new BufferedWriter(new OutputStreamWriter(out, StandardCharsets.US_ASCII))) {
|
||||||
|
writer.write("P3\n");
|
||||||
|
writer.write(String.valueOf(image.getWidth()));
|
||||||
|
writer.write(" ");
|
||||||
|
writer.write(String.valueOf(image.getHeight()));
|
||||||
|
writer.write("\n255\n");
|
||||||
|
|
||||||
|
for (int y = 0; y < image.getHeight(); y++) {
|
||||||
|
for (int x = 0; x < image.getWidth(); x++) {
|
||||||
|
var color = cs.encode(image.getRGB(x, y, cs));
|
||||||
|
writer.write(String.valueOf(color.red()));
|
||||||
|
writer.write(" ");
|
||||||
|
writer.write(String.valueOf(color.green()));
|
||||||
|
writer.write(" ");
|
||||||
|
writer.write(String.valueOf(color.blue()));
|
||||||
|
writer.write("\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -19,22 +19,21 @@ public final class ImageTexture implements Texture {
|
|||||||
private final int height;
|
private final int height;
|
||||||
private final @NotNull Spectrum[][] spectra;
|
private final @NotNull Spectrum[][] spectra;
|
||||||
|
|
||||||
public ImageTexture(@NotNull BufferedImage image, @NotNull ColorSpace cs, @NotNull Spectrum.Type type, boolean gamma) {
|
public ImageTexture(@NotNull BufferedImage image, @NotNull ColorSpace cs, @NotNull Spectrum.Type type) {
|
||||||
this.width = image.getWidth();
|
this.width = image.getWidth();
|
||||||
this.height = image.getHeight();
|
this.height = image.getHeight();
|
||||||
this.spectra = new Spectrum[height][width];
|
this.spectra = new Spectrum[height][width];
|
||||||
|
|
||||||
for (int y = 0; y < height; y++) {
|
for (int y = 0; y < height; y++) {
|
||||||
for (int x = 0; x < width; x++) {
|
for (int x = 0; x < width; x++) {
|
||||||
var rgb = new ColorRGB(image.getRGB(x, y));
|
var rgb = cs.decode(new ColorRGB(image.getRGB(x, y)));
|
||||||
if (gamma) rgb = ColorRGB.inverseGamma(rgb);
|
|
||||||
spectra[y][x] = cs.toSpectrum(rgb, type);
|
spectra[y][x] = cs.toSpectrum(rgb, type);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public ImageTexture(@NotNull String path, @NotNull ColorSpace cs) {
|
public ImageTexture(@NotNull String path, @NotNull ColorSpace cs) {
|
||||||
this(read(path), cs, Spectrum.Type.ALBEDO, true);
|
this(read(path), cs, Spectrum.Type.ALBEDO);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static @NotNull BufferedImage read(@NotNull String path) {
|
private static @NotNull BufferedImage read(@NotNull String path) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user