¿Cómo puedo implementar la iluminación en un motor voxel?

15

Estoy creando el MC como un motor de terreno, y he pensado que la iluminación haría que se viera mucho mejor. El problema es que los bloques no se iluminan correctamente cuando se coloca un bloque que emite luz (vea las capturas de pantalla en la parte inferior en la pagina.

Hasta ahora quiero implementar la iluminación "en bloque" de Minecraft. Entonces creé un 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;
        }
    }

Supongo que si quiero implementar la iluminación, tengo que especificar una luz para cada vértice ... Y ahora en mi archivo de efectos quiero poder tomar ese valor e iluminar el vértice en consecuencia:

float4x4 World;
float4x4 Projection;
float4x4 View;

Texture Texture;

sampler2D textureSampler = sampler_state  {
    Texture = <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();
    }
}

Y este es un ejemplo de cómo aplico la iluminación:

            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;

Y por último, aquí está el algorythim que lo calcula todo:

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

    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;
    }

Cualquier sugerencia y ayuda sería muy apreciada.

CAPTURAS DE PANTALLA Observe los artefactos en la parte superior del terreno y cómo solo la parte izquierda está parcialmente iluminada ... Intento de encender 1 Por alguna razón, solo ciertos lados del cubo se están iluminando y no ilumina el suelo Intento de encender 2

Otro ejemplo de lo anterior

Descubrí mi problema! No estaba comprobando si ese bloque ya estaba iluminado y, de ser así, en qué grado (si es más bajo, más alto)

    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);
        }
    }

Aunque lo anterior funciona, ¿alguien sabría cómo lo haría más eficiente?

Darestium
fuente

Respuestas:

17

He implementado algo similar a esto. Escribí una publicación al respecto en mi blog: byte56.com/2011/06/a-light-post . Pero entraré en un poco más de detalle aquí.

Si bien el artículo de codeflow vinculado en otra respuesta es bastante interesante. Por lo que entiendo, no es cómo Minecraft hace su iluminación. La iluminación de Minecraft es más autómatas celulares que la fuente de luz tradicional.

Supongo que estás familiarizado con el flujo de agua en MC. La iluminación en MC es esencialmente lo mismo. Te guiaré a través de un ejemplo simple.

Aquí hay algunas cosas a tener en cuenta.

  • Vamos a mantener una lista de cubos que necesitan verificar sus valores de iluminación
  • Solo los cubos transparentes y los cubos emisores de luz tienen valores de iluminación.

El primer cubo que agregamos es la fuente de luz. Una fuente es un caso especial. Su valor de luz se establece de acuerdo con el tipo de fuente de luz (por ejemplo, las antorchas obtienen un valor más brillante que la lava). Si un cubo tiene su valor de luz establecido por encima de 0, agregamos todos los cubos transparentes adyacentes a ese cubo a la lista. Para cada cubo en la lista, establecemos su valor de luz en su vecino más brillante menos uno. Esto significa que todos los cubos transparentes (esto incluye "aire") al lado de la fuente de luz obtienen un valor de luz de 15. Continuamos caminando los cubos alrededor de la fuente de luz, agregando cubos que necesitan ser revisados ​​y eliminando los cubos encendidos de la lista , util ya no tenemos ninguna para agregar. Eso significa que todos los últimos valores establecidos se han establecido en 0, lo que significa que hemos llegado al final de nuestra luz.

Esa es una explicación bastante simple de la iluminación. He hecho algo un poco más avanzado, pero comencé con el mismo principio básico. Este es un ejemplo de lo que produce:

ingrese la descripción de la imagen aquí

Ahora que tiene todo su conjunto de datos de luz. Cuando construya los valores de color para sus vértices, puede hacer referencia a estos datos de brillo. Podría hacer algo como esto (donde light es un valor int entre 0 y 15):

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

Básicamente estoy tomando el valor de la luz de 0 a 1 a la potencia de 1.4f. Esto me da un color oscuro más oscuro que una función lineal. Asegúrese de que su valor de color nunca supere 1. Lo hice dividiendo entre 16, en lugar de 15 para tener siempre un poco de espacio extra. Luego moví ese extra a la base para que siempre tuviera un poco de textura y no pura negrura.

Luego, en mi sombreador (similar a un archivo de efectos), obtengo el color del fragmento para la textura y lo multiplico por el color de iluminación que creo arriba. Esto significa que el brillo total proporciona la textura tal como fue creada. El brillo muy bajo da la textura como muy oscura (pero no negra debido al color base).

EDITAR

Para obtener la luz de una cara, miras el cubo en la dirección de la normalidad de la cara. Por ejemplo, la cara superior del cubo obtiene los datos de luz del cubo de arriba.

EDITAR 2

Intentaré abordar algunas de sus preguntas.

Entonces, ¿qué haría es algo así como la recursividad en este caso?

Puede usar un algoritmo recursivo o iterativo. Depende de usted cómo quiere implementarlo. Solo asegúrate de llevar un registro de los cubos que ya se agregaron, de lo contrario continuarás para siempre.

Además, ¿cómo se "enciende" el algoritmo?

Si habla de la luz solar, la luz solar es un poco diferente, ya que no queremos que disminuya el brillo. Los datos de mi cubo incluyen un bit SKY. Si un cubo está marcado como CIELO, eso significa que tiene acceso claro al cielo abierto sobre él. Los cubos SKY siempre obtienen iluminación completa menos el nivel de oscuridad. Luego, los cubos al lado del cielo, que no son cielo, como entradas de cuevas o voladizos, se hace cargo del procedimiento normal de iluminación. Si solo estás hablando de un punto de luz que brilla hacia abajo ... es lo mismo que cualquier otra dirección.

¿Cómo especificaría la luz solo para una cara?

No especificas la luz solo para una sola cara. Cada cubo transparente especifica la luz para todos los cubos sólidos que comparten una cara con él. Si desea obtener la luz para una cara, simplemente verifique el valor de la luz para el cubo transparente que está tocando. Si no está tocando un cubo transparente, entonces no lo estaría renderizando de todos modos.

Código de muestras?

No

MichaelHouse
fuente
@ Byte56 Me encantaría saber cómo traducir este algoritmo para que funcione en "fragmentos".
gopgop
Pensé en actualizar cada lado del fragmento de acuerdo con el vecino y luego agregar todos los bloques cambiados a la lista de bloques para cambiar, pero parece que no funciona
gopgop
@gopgop Los comentarios no son el lugar para la discusión. Puedes encontrarme en el chat en algún momento. O discuta con las otras personas allí.
MichaelHouse
4

Lea el siguiente artículo, ya que debería darle mucha información sobre lo que busca. Hay muchas secciones sobre iluminación, pero en particular lea las secciones sobre Oclusión ambiental, Luz de observación y Recolección de luz:

http://codeflow.org/entries/2010/dec/09/minecraft-like-rendering-experiments-in-opengl-4/

Pero tratando de responder el núcleo de su pregunta:

  • Si desea oscurecer un color, multiplíquelo por otro color (o simplemente por un flotador entre 0 y 1 si desea oscurecer todos los componentes por igual), donde 1 en un componente representa la intensidad completa mientras que 0 representa la oscuridad total.
  • Si desea aclarar un color, agregue otro color y sujete el resultado. Pero tenga cuidado de no elegir valores que sean tan altos que hagan que el resultado se sature a blanco puro. Elija colores oscuros, con un toque del tono que está buscando, como un naranja oscuro (# 886600).

    o...

    Después de agregar el color, en lugar de sujetar el resultado (es decir, sujetar cada componente del color entre 0 y 1), escale el resultado; encuentre cuál de los tres componentes (RGB) es el más grande y divídalos por ese valor. Este método conserva las propiedades originales del color un poco mejor.

David Gouveia
fuente