# How can I make 3D lighting smooth?

I'm trying to view 3D objects in a terminal. Everything was looking good until I implemented lighting. I'm using shadow rays to determine where the object should be lit. Although it's working I don't know how to make it smooth, I haven't tried anything since I don't actually know what to do.

Here's how a sphere is looking (light is coming from above): As you can see it's either really dark (.) or light (@) so it's hard to determine the object.

• Can you unpack for us 1) how you are using shadow rays to compute the brightness at a point, and 2) how you are converting brightness into an ASCII character? Mar 19, 2021 at 16:21
• @DMGregory I'm casting rays from the camera, when a ray hits an object, i cast another ray in the direction of the light source (a (0, 1, 0) ray in this case) if there's nothing in the way of it then i set the lighting level to the maximum which is 11 if not i set it to 0 when i'm drawing the view i use this string (".,-~:;=!*#\$@") with the lighting level as the index of the character Mar 19, 2021 at 16:28
• Why do we see such a sporadic pattern of . and @ on the upper hemisphere here, rather than the top half being all fully-lit? Do you have a lot of small objects above the sphere, that are blocking the light rays and casting blotchy shadows onto the sphere? Or is there some noise or dithering in your ray? Or does the sphere itself have some undulations to it, like a planet with mountains? Mar 19, 2021 at 17:26
• The sphere is blockey, I have a 3D array of characters and i use a distance function to fill up the array, so i cant use floats which make small circles really blockey, that's how i represent the sphere, you just gave me an idea maybe i should stop representing the sphere as a 3D array and start representing it as a distance function if the distance between the ray and sphere is less or equal than the radius of the sphere then the ray is intersecting the sphere Mar 19, 2021 at 18:08
• One approach to at least smooth the lighting by one "pixel" (character) is to use simple blending. E.g. for adjacent pixels a and b: if (a.brightness == 0 && b.brightness == 11) a.brightness = 5 Mar 19, 2021 at 19:16

The sphere is blockey, I have a 3D array of characters and i use a distance function to fill up the array, so i cant use floats which make small circles really blockey, that's how i represent the sphere, you just gave me an idea maybe i should stop representing the sphere as a 3D array and start representing it as a distance function if the distance between the ray and sphere is less or equal than the radius of the sphere then the ray is intersecting the sphere

In your comment above, it looks like you've hit on the key insight you need for smooth lighting.

Given a ray, and a sphere's center point and radius, you can detect precisely where the ray hits the sphere, using algorithms like the ones in this past Q&A.

Now that you have a point on the sphere's surface, you can compute the surface normal at that point: a unit vector pointing perpendicular to the surface, directly out from the sphere. If your intersection routine doesn't already generate this, you can deduce it from the position of the reported intersection point:

 Vector3 normal = Vector3.Normalize(rayHitPosition - sphereCenter);


(Here, Vector3.Normalize() refers to the operation of dividing all components of a vector by that vector's length, to create a unit vector with length 1 in the same direction as the original)

You can compute the direction to the light source the same way:

 Vector3 toLight = Vector3.Normalize(lightPosition - rayHitPosition)


Given these two vectors, we can compute the diffuse illumination at this location. That's the scattered light you get bouncing off of a matte surface, like plaster. It's brightest when the light is hitting the surface dead-on (when the toLight and normal vectors point in the same direction), and fades as the surface turns away from the light, catching it just edge-on. We can model that with what's called a Lambertian term:

 float diffuse = Mathf.Max(Vector3.Dot(toLight, normal), 0);


Here Vector3.Dot() is the scalar product, multiplying the x component of one vector by the x component of the other, then y with y, z with z, and adding the three products together to make one number. Because we used unit vectors, that number is the cosine of the angle between the vectors - ie. it goes from 1 when they're pointing in the same direction, to 0 when they're perpendicular, to -1 when they're pointing in opposite directions.

I use a clamping function so that any values below zero - corresponding to the surface facing away from the light source - produce zero scattered light, instead of "negative light".

This will give you a smoothly varying value, from 1 on the part of your sphere closest to the light, and getting darker and darker until it hits zero at the terminator - the equator separating the lit portion of the sphere from the shadowed half.

You can scale this diffuse component by the brightness (or colour) of your light source, relative to the exposure of your image, then map the resulting value into your character range for display.

We can use simple vector tools like this to compute additional lighting terms to get different surface appearances - like adding the specular reflections you'd see on shiny surfaces, or metallic reflections you'd see from metal, but those might not be very noticeable in your text-based representation.

• Thank you, I already implemented the distance function thing i was talking about before and the sphere is looking much better and smoother, I've read this and i think i know how it works and how to write it, ill try it asap. Mar 20, 2021 at 19:10
• I've been playing with this for a bit, Do i implement shadows the way it was (if there's something in the way of the light the diffuse would be 0) or is there some better way?? And also my light is directional so I've been trying to use a vector with the cos as the X and the sin as the Y of my light angle, But it doesn't look good with big objects. Mar 21, 2021 at 19:13
• The light vector for a directional light just points in the direction opposite the direction the light is shining (it points "upstream"), the same as the ray you fire to check for shadow casters. If a point has been occluded by a shadow caster, then there's no incoming light to bounce and scatter, so you indeed leave it at zero. Mar 21, 2021 at 19:15
• Thank you very much here's how its looking right now for everyone to see the difference i.imgur.com/IIjAofR.png Mar 21, 2021 at 19:27