Skip to content

Commit

Permalink
add image sphere
Browse files Browse the repository at this point in the history
  • Loading branch information
boxofyellow committed Sep 11, 2022
1 parent 37ecd98 commit 0a83dbe
Show file tree
Hide file tree
Showing 13 changed files with 185 additions and 53 deletions.
19 changes: 2 additions & 17 deletions Actors/ImagePlane.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,22 +43,7 @@ public override ColorProperties ColorAt(Point3D intersection, int id)
var row = Math.Clamp((int)Math.Floor(intersection.Y), 0, m_colorData.Length - 1);
var col = Math.Clamp((int)Math.Floor(intersection.X), 0, m_colorData[0].Length - 1);

Argb32 cell = m_colorData[row][col];

if (cell.A > 0)
{
return new ColorProperties(
ColorProperties.WhitePlastic.Ambient,
diffuse: new Point3D(cell.R / 255.0, cell.G / 255.0, cell.B / 255.0 ),
ColorProperties.WhitePlastic.Specular,
ColorProperties.WhitePlastic.Shininess);
}
else
{
// for transparent pixels just mark them as polished silver
// TODO: one thing that ray trace is known for is dealing with transparent object, so we could do something better here...
return ColorProperties.PolishedSilver;
}
return ColorProperties.Plastic(m_colorData[row][col]);
}

private static (Point3D[] Points, int[][] Faces) GetData(Settings settings, out Point3D offset, out Argb32[][] colorData)
Expand All @@ -67,7 +52,7 @@ private static (Point3D[] Points, int[][] Faces) GetData(Settings settings, out
var widthOver2 = image.Width / 2.0;
var hightOver2 = image.Height / 2.0;

offset = new Point3D(widthOver2, hightOver2, 0);
offset = new(widthOver2, hightOver2, 0);

colorData = new Argb32[image.Height][];
for (int i = 0; i < image.Height; i++)
Expand Down
53 changes: 53 additions & 0 deletions Actors/ImageSphere.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
using System;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.PixelFormats;

namespace Ascii3dEngine
{
public class ImageSphere : Sphere
{
public ImageSphere(Settings setting, Point3D center, double radius) : base(setting, center, radius)
{
using Image<Argb32> image = Image.Load<Argb32>(setting.ImageSphereFile);

m_colorData = new Argb32[image.Height][];
for (int i = 0; i < image.Height; i++)
{
// Row 0 is the top, but normally we would want the top to the rows with the higher number;
m_colorData[i] = image.GetPixelRowSpan(image.Height - 1 - i).ToArray();
}
}

public override ColorProperties ColorAt(Point3D intersection, int id)
{
const double piOver4 = Math.PI / 4.0;
const double piOver2 = Math.PI / 2.0;

intersection = Motion.Unapply(intersection);

//https://stackoverflow.com/questions/5674149/3d-coordinates-on-a-sphere-to-latitude-and-longitude
// we already unapplied out motion matrix (which undid the scalding), which will map our points on a sphere centered at the origin with a radius of 1
// r = √(x² + y² + z²) = 1
// θ = cos-1(z/r), (90° - θ) your latitude (negative means it's on the bottom) as it's measured from top.
// 𝜑 = tan-1(x/y)
// But for their coordinate system Z was "up" ∴ we will need to swap Y and Z

var latitude = Math.Acos(intersection.Y) - piOver2;
var longitude = Math.Atan2(intersection.X, intersection.Z);

// https://en.wikipedia.org/wiki/Web_Mercator_projection
var x = longitude + Math.PI; // x = λ + Math.PI
var y = Math.PI - Math.Log(Math.Tan(piOver4 + latitude / 2.0)); // y = π - ln(tan(π/4 + 𝜑 / 2))
// This should yield 0 ≤ x ≤ 2π and 0 ≤ y ≤ 2π

// It did mention a 𝜑MAX (as 2 * tan-1(𝑒^π) - π/2), but that never needed, But it looks like that gets managed with the Clamp here
int row = Math.Clamp((int)Math.Floor(y * (double)m_colorData.Length / Math.Tau), 0, m_colorData.Length - 1);
int col = Math.Clamp((int)Math.Floor(x * (double)m_colorData[0].Length / Math.Tau), 0, m_colorData[0].Length - 1);

return ColorProperties.Plastic(m_colorData[row][col]);
}

private readonly Argb32[][] m_colorData;
}
}
12 changes: 12 additions & 0 deletions Actors/SolidSphere.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace Ascii3dEngine
{
public class SolidSphere : Sphere
{
public SolidSphere(Settings settings, Point3D center, double radius, ColorProperties properties)
: base(settings, center, radius) => m_properties = properties;

public override ColorProperties ColorAt(Point3D intersection, int id) => m_properties;

private readonly ColorProperties m_properties;
}
}
50 changes: 39 additions & 11 deletions Actors/Sphere.cs
Original file line number Diff line number Diff line change
@@ -1,20 +1,36 @@
using System;
using System.Runtime.CompilerServices;

namespace Ascii3dEngine
{
public class Sphere : Actor
public abstract class Sphere : Actor
{
public Sphere(Point3D center, double radius, ColorProperties properties)
public Sphere(Settings settings, Point3D center, double radius)
{
m_spin = settings.Spin;
m_id = ReserveIds(1);
Center = center;
m_radius = radius;
m_properties = properties;
Motion
.MoveTo(center)
.SetScale(Point3D.Identity * radius);
}

m_rSquared = m_radius * m_radius;
public override void Act(TimeSpan timeDelta, TimeSpan elapsedRuntime, Camera camera)
{
if (m_spin)
{
Motion.RotateByY(timeDelta.TotalSeconds * c_15degreesRadians);
}
}

public override ColorProperties ColorAt(Point3D intersection, int id) => m_properties;
public override void StartRayRender(Point3D from, LightSource[] sources)
{
if (Motion.Scale.X != Motion.Scale.Y || Motion.Scale.X != Motion.Scale.Z)
{
throw new Exception("Can only scale spheres in all directions at once");
}
m_rSquared = Motion.Scale.X * Motion.Scale.X;
base.StartRayRender(from, sources);
}

// Gosh Spheres are easy :) if the sphere was centered at the origin the normal is just the intersection point.
public override Point3D NormalAt(Point3D intersection, int id) => intersection - Center;
Expand Down Expand Up @@ -104,11 +120,23 @@ public override bool DoesItCastShadow(int sourceIndex, Point3D from, Point3D vec
return default;
}

protected Point3D Center;
private readonly bool m_spin;

protected MotionMatrix Motion = new();

protected Point3D Center
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => Motion.Translation;

[MethodImpl(MethodImplOptions.AggressiveInlining)]
set => Motion.MoveTo(value);
}

private double m_rSquared;

private readonly int m_id;
private readonly double m_radius;
private readonly double m_rSquared;
private readonly ColorProperties m_properties;

private readonly static double c_15degreesRadians = Utilities.DegreesToRadians(15);
}
}
11 changes: 11 additions & 0 deletions Ascii3dEngine.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,17 @@
<!-- From https://www.pngwing.com/en/free-png-zordq -->
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>

<Content Include="earth.png">
<!--
From https://www.researchgate.net/figure/The-web-Mercator-projection_fig5_298354278
- Cropped to remove the boarder
- Colored water blue
- Colored land masses green or white
- removed longitude/latitude lines
-->
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>

</Project>
19 changes: 19 additions & 0 deletions Engine/ColorProperties.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.PixelFormats;

namespace Ascii3dEngine
{
Expand Down Expand Up @@ -118,5 +119,23 @@ public ColorProperties(Point3D ambient, Point3D diffuse, Point3D specular, doubl
new(0.55, 0.0, 0.55),
new(0.7, 0.7, 0.7),
32);

public static ColorProperties Plastic(Argb32 color)
{
if (color.A > 0)
{
return new (
WhitePlastic.Ambient,
diffuse: new(color.R / 255.0, color.G / 255.0, color.B / 255.0),
WhitePlastic.Specular,
WhitePlastic.Shininess);
}
else
{
// for transparent pixels just mark them as polished silver
// TODO: one thing that ray trace is known for is dealing with transparent object, so we could do something better here...
return PolishedSilver;
}
}
}
}
35 changes: 18 additions & 17 deletions Engine/MotionMatrix.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,15 @@ public Point3D Apply(Point3D point)
}

point = new(
point.X * m_scale.X,
point.Y * m_scale.Y,
point.Z * m_scale.Z
point.X * Scale.X,
point.Y * Scale.Y,
point.Z * Scale.Z
);

return new(
point.X * m_rotationMatrix[0, 0] + point.Y * m_rotationMatrix[0, 1] + point.Z * m_rotationMatrix[0, 2] + m_translation.X,
point.X * m_rotationMatrix[1, 0] + point.Y * m_rotationMatrix[1, 1] + point.Z * m_rotationMatrix[1, 2] + m_translation.Y,
point.X * m_rotationMatrix[2, 0] + point.Y * m_rotationMatrix[2, 1] + point.Z * m_rotationMatrix[2, 2] + m_translation.Z
point.X * m_rotationMatrix[0, 0] + point.Y * m_rotationMatrix[0, 1] + point.Z * m_rotationMatrix[0, 2] + Translation.X,
point.X * m_rotationMatrix[1, 0] + point.Y * m_rotationMatrix[1, 1] + point.Z * m_rotationMatrix[1, 2] + Translation.Y,
point.X * m_rotationMatrix[2, 0] + point.Y * m_rotationMatrix[2, 1] + point.Z * m_rotationMatrix[2, 2] + Translation.Z
);
}

Expand All @@ -53,17 +53,17 @@ public Point3D Unapply(Point3D point)
return point;
}

point -= m_translation;
point -= Translation;
point = new(
point.X * m_rotationMatrix[0, 0] + point.Y * m_rotationMatrix[1, 0] + point.Z * m_rotationMatrix[2, 0],
point.X * m_rotationMatrix[0, 1] + point.Y * m_rotationMatrix[1, 1] + point.Z * m_rotationMatrix[2, 1],
point.X * m_rotationMatrix[0, 2] + point.Y * m_rotationMatrix[1, 2] + point.Z * m_rotationMatrix[2, 2]
);

return new (
point.X / m_scale.X,
point.Y / m_scale.Y,
point.Z / m_scale.Z
point.X / Scale.X,
point.Y / Scale.Y,
point.Z / Scale.Z
);
}

Expand All @@ -72,21 +72,21 @@ public Point3D Unapply(Point3D point)
public MotionMatrix MoveTo(Point3D point)
{
m_isIdentity = false;
m_translation = point;
Translation = point;
return this;
}

public MotionMatrix MoveBy(Point3D point)
{
m_isIdentity = false;
m_translation += point;
Translation += point;
return this;
}

public MotionMatrix SetScale(Point3D scale)
{
m_isIdentity = false;
m_scale = scale;
Scale = scale;
return this;
}

Expand Down Expand Up @@ -209,7 +209,7 @@ public MotionMatrix RotateByZ(double angle)
var c = Math.Cos(angle);
var s = Math.Sin(angle);

var result = new double[,]
m_rotationMatrix = new double[,]
{
{
c * m_rotationMatrix[0, 0] - s * m_rotationMatrix[1, 0], // c[0,0] - s[1,0] + 0[2, 0]
Expand All @@ -231,6 +231,10 @@ public MotionMatrix RotateByZ(double angle)
return this;
}

public Point3D Translation { get; private set; }

public Point3D Scale { get; private set; } = Point3D.Identity;

// TODO: should we make changes to help limit the number of arrays that allocate, should we change this to only allocate
// this one on the heep and all the "temp" ons the stack?
private double[,] m_rotationMatrix = new double[,]
Expand All @@ -240,9 +244,6 @@ public MotionMatrix RotateByZ(double angle)
{Point3D.ZUnit.X, Point3D.ZUnit.Y, Point3D.ZUnit.Z},
};

private Point3D m_translation;
private Point3D m_scale = new(1, 1, 1);

private bool m_isIdentity = true;
}
}
17 changes: 14 additions & 3 deletions Engine/Point3D.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public static Point3D Parse(string? value, Point3D? defaultValue = null)
{
if (string.IsNullOrEmpty(value))
{
return defaultValue ?? new();
return defaultValue ?? Zero;
}

string temp = value.TrimStart('{').TrimEnd('}');
Expand All @@ -40,12 +40,18 @@ public static Point3D Parse(string? value, Point3D? defaultValue = null)
// since we will often be dividing by this (see Normalized) we might want to use Quake's fast InvSqrt function (https://en.wikipedia.org/wiki/Fast_inverse_square_root)
// But that is not really going to get us much in the way of savings see https://stackoverflow.com/questions/268853/is-it-possible-to-write-quakes-fast-invsqrt-function-in-c
// Additionally we should also just be mindful to see if we really do need the Square at all see ColorUtilities.BestMatch
public double Length => Math.Sqrt(X * X + Y * Y + Z * Z);
public double Length {
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => Math.Sqrt(X * X + Y * Y + Z * Z);
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Point3D Normalized() => this / Length;

public bool IsZero => X == 0.0 && Y == 0.0 && Z == 0.0;
public bool IsZero {
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => this == Zero;
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Point3D CrossProduct(Point3D vector) => new(
Expand Down Expand Up @@ -115,12 +121,17 @@ public override bool Equals(object? obj)
public override int GetHashCode()
=> X.GetHashCode() ^ Y.GetHashCode() ^ Z.GetHashCode();


public readonly static Point3D Zero = new();

public readonly static Point3D XUnit = new(1, 0, 0);

public readonly static Point3D YUnit = new(0, 1, 0);

public readonly static Point3D ZUnit = new(0, 0, 1);

public readonly static Point3D Identity = new(XUnit.X, YUnit.Y, ZUnit.Z);

public override string ToString() => $"{{{X}, {Y}, {Z}}}";
}
}
5 changes: 5 additions & 0 deletions Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,11 @@ private static int Run(Settings settings)
scene.AddActor(ImagePlane.Create(settings, center: scene.Camera.To, normal, up, scale: settings.ImageScale));
}

if (!string.IsNullOrEmpty(settings.ImageSphereFile))
{
scene.AddActor(new ImageSphere(settings, scene.Camera.To, settings.ImageSphereRadius));
}

if (!settings.Axes && !settings.Cube && !scene.HasActors)
{
settings.Axes = true;
Expand Down
6 changes: 6 additions & 0 deletions Settings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@ public class Settings
[Option(nameof(ImageScale))]
public double ImageScale { get; set; } = 1;

[Option(nameof(ImageSphereFile))]
public string? ImageSphereFile { get; set; }

[Option(nameof(ImageSphereRadius))]
public double ImageSphereRadius { get; set; } = 1;

[Option(nameof(To))]
public string? To { get; set; }

Expand Down
Loading

0 comments on commit 0a83dbe

Please sign in to comment.