Wednesday, June 13, 2018

c# - How can I implement lighting in a voxel engine?


I am creating the MC like terrain engine, and I have thought that lighting would make it look a whole lot nicer.The problem is that the blocks are not being lit properly when a block which emits light is placed (see the screenshots at the bottom on the page.


So far I want to implement minecraft's "blocky" lighting. So I created a VertexFormat:


 struct VertexPositionTextureLight
{
Vector3 position;

Vector2 textureCoordinates;
float light;

public readonly static VertexDeclaration VertexDeclaration = new VertexDeclaration
(
new VertexElement(0, VertexElementFormat.Vector3, VertexElementUsage.Position, 0),
new VertexElement(sizeof(float) * 3, VertexElementFormat.Vector2, VertexElementUsage.TextureCoordinate, 0),
new VertexElement(sizeof(float) * 5, VertexElementFormat.Single, VertexElementUsage.TextureCoordinate, 1)
);


public VertexPositionTextureLight(Vector3 position, Vector3 normal, Vector2 textureCoordinate, float light)
{
// I don't know why I included normal data :)
this.position = position;
this.textureCoordinates = textureCoordinate;
this.light = light;
}
}

I guess if I want to implement lighting I have to specify a light for each vertex... And now in my effect file I want to be able to take that value and light up the vertex accordingly:



float4x4 World;
float4x4 Projection;
float4x4 View;

Texture Texture;

sampler2D textureSampler = sampler_state {
Texture = ;
MipFilter = Point;
MagFilter = Point;

MinFilter = Point;
AddressU = Wrap;
AddressV = Wrap;
};

struct VertexToPixel {
float4 Position : POSITION;
float4 TexCoords : TEXCOORD0;
float4 Light : TEXCOORD01;
};


struct PixelToFrame {
float4 Color : COLOR0;
};

VertexToPixel VertexShaderFunction(float4 inPosition : POSITION, float4 inTexCoords : TEXCOORD0, float4 light : TEXCOORD01) {
VertexToPixel Output = (VertexToPixel)0;

float4 worldPos = mul(inPosition, World);
float4 viewPos = mul(worldPos, View);


Output.Position = mul(viewPos, Projection);
Output.TexCoords = inTexCoords;
Output.Light = light;

return Output;
}

PixelToFrame PixelShaderFunction(VertexToPixel PSIn) {
PixelToFrame Output = (PixelToFrame)0;


float4 baseColor = 0.086f;
float4 textureColor = tex2D(textureSampler, PSIn.TexCoords);
float4 colorValue = pow(PSIn.Light / 16.0f, 1.4f) + baseColor;

Output.Color = textureColor;

Output.Color.r *= colorValue;
Output.Color.g *= colorValue;
Output.Color.b *= colorValue;

Output.Color.a = 1;

return Output;
}

technique Block {
pass Pass0 {
VertexShader = compile vs_2_0 VertexShaderFunction();
PixelShader = compile ps_2_0 PixelShaderFunction();
}

}

VertexToPixel VertexShaderBasic(float4 inPosition : POSITION, float4 inTexCoords : TEXCOORD0) {
VertexToPixel Output = (VertexToPixel)0;

float4 worldPos = mul(inPosition, World);
float4 viewPos = mul(worldPos, View);

Output.Position = mul(viewPos, Projection);
Output.TexCoords = inTexCoords;


return Output;
}

PixelToFrame PixelShaderBasic(VertexToPixel PSIn) {
PixelToFrame Output = (PixelToFrame)0;

Output.Color = tex2D(textureSampler, PSIn.TexCoords);

return Output;

}


technique Basic {
pass Pass0 {
VertexShader = compile vs_2_0 VertexShaderBasic();
PixelShader = compile ps_2_0 PixelShaderBasic();
}
}


And this is an example on how i apply lighting:


            case BlockFaceDirection.ZDecreasing:
light = world.GetLight((int)(backNormal.X + pos.X), (int)(backNormal.Y + pos.Y), (int)(backNormal.Z + pos.Z));

SolidVertices.Add(new VertexPositionTextureLight(bottomRightBack, backNormal, bottomLeft, light));
SolidVertices.Add(new VertexPositionTextureLight(bottomLeftBack, backNormal, bottomRight, light));
SolidVertices.Add(new VertexPositionTextureLight(topRightBack, backNormal, topLeft, light));
SolidVertices.Add(new VertexPositionTextureLight(topLeftBack, backNormal, topRight, light));
AddIndices(0, 2, 3, 3, 1, 0);
break;


And last of all here is the algorythim that calculates it all:


    public void AddCubes(Vector3 location, float light)
{
AddAdjacentCubes(location, light);
Blocks = new List();
}

public void Update(World world)
{

this.world = world;
}

public void AddAdjacentCubes(Vector3 location, float light)
{
if (light > 0 && !CubeAdded(location))
{
world.SetLight((int)location.X, (int)location.Y, (int)location.Z, (int)light);
Blocks.Add(location);


// Check ajacent cubes
for (int x = -1; x <= 1; x++)
{
for (int y = -1; y <= 1; y++)
{
for (int z = -1; z <= 1; z++)
{
// Make sure the cube checked it not the centre one
if (!(x == 0 && y == 0 && z == 0))
{

Vector3 abs_location = new Vector3((int)location.X + x, (int)location.Y + y, (int)location.Z + z);

// Light travels on transparent block ie not solid
if (!world.GetBlock((int)location.X + x, (int)location.Y + y, (int)location.Z + z).IsSolid)
{
AddAdjacentCubes(abs_location, light - 1);
}
}
}
}

}

}
}

public bool CubeAdded(Vector3 location)
{
for (int i = 0; i < Blocks.Count; i++)
{
if (location.X == Blocks[i].X &&

location.Y == Blocks[i].Y &&
location.Z == Blocks[i].Z)
{
return true;
}
}

return false;
}


Any suggestions and help would be much appreciated


SCREENSHOTS Notice the artifacts on the top on the terrain and how only the left part is partically lit... Attempt at lighting 1 For some reason only certain sides of the cube is being lit and it doesn't light the ground Attempt at lighting 2


Another example of the previous


Figured out my problem! I was not checking if that block was already lit and if so to what degree (if it is lower light it higher)


    public void DoLight(int x, int y, int z, float light)
{
Vector3 xDecreasing = new Vector3(x - 1, y, z);
Vector3 xIncreasing = new Vector3(x + 1, y, z);
Vector3 yDecreasing = new Vector3(x, y - 1, z);
Vector3 yIncreasing = new Vector3(x, y + 1, z);

Vector3 zDecreasing = new Vector3(x, y, z - 1);
Vector3 zIncreasing = new Vector3(x, y, z + 1);

if (light > 0)
{
light--;

world.SetLight(x, y, z, (int)light);
Blocks.Add(new Vector3(x, y, z));


if (world.GetLight((int)yDecreasing.X, (int)yDecreasing.Y, (int)yDecreasing.Z) < light &&
world.GetBlock((int)yDecreasing.X, (int)yDecreasing.Y, (int)yDecreasing.Z).BlockType == BlockType.none)
DoLight(x, y - 1, z, light);
if (world.GetLight((int)yIncreasing.X, (int)yIncreasing.Y, (int)yIncreasing.Z) < light &&
world.GetBlock((int)yIncreasing.X, (int)yIncreasing.Y, (int)yIncreasing.Z).BlockType == BlockType.none)
DoLight(x, y + 1, z, light);
if (world.GetLight((int)xDecreasing.X, (int)xDecreasing.Y, (int)xDecreasing.Z) < light &&
world.GetBlock((int)xDecreasing.X, (int)xDecreasing.Y, (int)xDecreasing.Z).BlockType == BlockType.none)
DoLight(x - 1, y, z, light);
if (world.GetLight((int)xIncreasing.X, (int)xIncreasing.Y, (int)xIncreasing.Z) < light &&

world.GetBlock((int)xIncreasing.X, (int)xIncreasing.Y, (int)xIncreasing.Z).BlockType == BlockType.none)
DoLight(x + 1, y, z, light);
if (world.GetLight((int)zDecreasing.X, (int)zDecreasing.Y, (int)zDecreasing.Z) < light &&
world.GetBlock((int)zDecreasing.X, (int)zDecreasing.Y, (int)zDecreasing.Z).BlockType == BlockType.none)
DoLight(x, y, z - 1, light);
if (world.GetLight((int)zIncreasing.X, (int)zIncreasing.Y, (int)zIncreasing.Z) < light &&
world.GetBlock((int)zIncreasing.X, (int)zIncreasing.Y, (int)zIncreasing.Z).BlockType == BlockType.none)
DoLight(x, y, z + 1, light);
}
}


Altough the above works, would anyone know how I would make it more performace efficent?



Answer



I've implemented something similar to this. I wrote up a post about it on my blog: byte56.com/2011/06/a-light-post. But I'll go into a little more detail here.


While the codeflow article linked in another answer is pretty interesting. From what I understand, it's not how Minecraft does its lighting. Minecraft lighting is more cellular automata then traditional light source.


I assume you're familiar with water flow in MC. Lighting in MC is essentially the same thing. I'll walk you through a simple example.


Here are a few things to keep in mind.



  • We're going to keep a list of cubes that need their lighting values checked

  • Only transparent cubes and light emitting cubes have lighting values



The first cube we add is the light source. A source is a special case. It's light value is set accordingly to the light source type (for example torches get a brighter value than lava). If a cube has its light value set above 0, we add all the transparent cubes adjacent to that cube to the list. For each cube on the list, we set its light value to its brightest neighbor minus one. This means that all the transparent cubes (this includes "air") next to the light source get a light value of 15. We continue walking the cubes around the light source, adding cubes that need to be checked and taking lit cubes off the list, util we no longer have any to add. That means that all the latest values set have been set to 0, which means we've reached the end of our light.


That's a fairly simple explanation of lighting. I've done something a little more advanced, but I started with the same basic principle. This is an example of what it produces:


enter image description here


Now that you have all your light data set. When you're building the color values for your vertices, you can reference this brightness data. You could do something like this (where light is an int value between 0 and 15):


float baseColor = .086f;
float colorValue = (float) (Math.pow(light / 16f, 1.4f) + baseColor );
return new Color(colorValue, colorValue, colorValue, 1);

Basically I'm taking the light value from 0 to 1 to the power of 1.4f. This gives me a slightly darker darks than a linear function. Ensure that your color value never goes above 1. I did that by dividing by 16, instead of 15 so I'd always have a little extra room. Then moved that extra to the base so I'd always have a little texture and not pure blackness.



Then in my shader (similar to an effects file), I get the fragment color for the texture and multiply that by the lighting color I create above. This means full brightness gives the texture as it was created. Very low brightness gives the texture as very dark (but not black because of the base color).


EDIT


To get the light for a face, you look at the cube in the direction of the normal of the face. For example, top face of cube gets the light data from the cube above.


EDIT 2


I'll attempt to address some of your questions.


So what I would do is something like recursion in this case?


You can use a recursive algorithm or iterative. It's up you you how you want to implement it. Just ensure you're keeping track of which cubes have been added already, otherwise you'll keep going forever.


Also how would the algorithm "light downwards"?


If you're talking about sunlight, sunlight is a bit different, since we don't want it to decrease in brightness. My cube data includes a SKY bit. If a cube is marked as SKY that means it has clear access to the open sky above it. SKY cubes always get full lighting minus the darkness level. Then cubes next to sky, that are not sky, like cave entrances or overhangs, the normal lighting procedure takes over. If you're just talking about a point light shining down... it's the same as any other direction.


How would I specify the light for a single face only?



You don't specify light for a single face only. Each transparent cube specifies the light for all the solid cubes that share a face with it. If you want to get the light for a face, simply check the light value for the transparent cube it's touching. If it's not touching a transparent cube, then you wouldn't be rendering it anyway.


Code samples?


Nope.


No comments:

Post a Comment

Simple past, Present perfect Past perfect

Can you tell me which form of the following sentences is the correct one please? Imagine two friends discussing the gym... I was in a good s...