Como posso implementar a iluminação em um mecanismo voxel?

15

Estou criando o MC como um mecanismo de terreno, e achei que a iluminação faria com que parecesse muito melhor. O problema é que os blocos não estão sendo iluminados corretamente quando um bloco que emite luz é colocado (veja as imagens na parte inferior na página.

Até agora, eu quero implementar a iluminação "em blocos" do minecraft. Então eu criei um 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;
        }
    }

Acho que se eu quiser implementar a iluminação, tenho que especificar uma luz para cada vértice ... E agora no meu arquivo de efeito, quero poder pegar esse valor e iluminar o vértice de acordo:

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

E este é um exemplo de como aplico iluminação:

            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;

E, finalmente, aqui está o algorythim que calcula tudo:

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

Todas as sugestões e ajuda serão muito apreciadas

TELA Observe os artefatos no topo do terreno e como apenas a parte esquerda é iluminada parcialmente ... Tentativa de iluminação 1 Por alguma razão, apenas certos lados do cubo estão sendo iluminados e não iluminam o chão Tentativa de iluminação 2

Outro exemplo do anterior

Descobri o meu problema! Eu não estava verificando se esse bloco já estava aceso e, em caso afirmativo, em que grau (se for menor a luz, mais 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);
        }
    }

Apesar das obras acima, alguém saberia como eu tornaria mais eficiente o desempenho?

Darestium
fonte

Respostas:

17

Eu implementei algo semelhante a isso. Eu escrevi um post sobre isso no meu blog: byte56.com/2011/06/a-light-post . Mas vou entrar em mais detalhes aqui.

Embora o artigo do fluxo de código vinculado em outra resposta seja bastante interessante. Pelo que entendi, não é como o Minecraft faz sua iluminação. A iluminação do Minecraft é mais autômatos celulares do que a fonte de luz tradicional.

Suponho que você esteja familiarizado com o fluxo de água no MC. A iluminação no MC é essencialmente a mesma coisa. Vou orientá-lo através de um exemplo simples.

Aqui estão algumas coisas a serem lembradas.

  • Vamos manter uma lista de cubos que precisam de seus valores de iluminação verificados
  • Somente cubos transparentes e cubos emissores de luz têm valores de iluminação

O primeiro cubo que adicionamos é a fonte de luz. Uma fonte é um caso especial. Seu valor de luz é definido de acordo com o tipo de fonte de luz (por exemplo, as tochas têm um valor mais brilhante que a lava). Se um cubo tiver seu valor de luz definido acima de 0, adicionamos à lista todos os cubos transparentes adjacentes a esse cubo. Para cada cubo da lista, definimos seu valor de luz como seu vizinho mais brilhante menos um. Isso significa que todos os cubos transparentes (incluindo "ar") próximos à fonte de luz recebem um valor de luz 15. Continuamos caminhando pelos cubos em torno da fonte de luz, adicionando cubos que precisam ser verificados e retirando os cubos acesos da lista , util, não temos mais a adicionar. Isso significa que todos os valores mais recentes definidos foram definidos como 0, o que significa que chegamos ao fim da nossa luz.

Essa é uma explicação bastante simples da iluminação. Fiz algo um pouco mais avançado, mas comecei com o mesmo princípio básico. Este é um exemplo do que produz:

insira a descrição da imagem aqui

Agora que você tem todos os seus dados de luz. Ao criar os valores de cores para seus vértices, você pode fazer referência a esses dados de brilho. Você poderia fazer algo assim (onde light é um valor int entre 0 e 15):

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

Basicamente, estou levando o valor da luz de 0 a 1 para a potência de 1.4f. Isso me dá um sombrio um pouco mais escuro do que uma função linear. Garanta que seu valor de cor nunca fique acima de 1. Fiz isso dividindo por 16, em vez de 15, para ter sempre um pouco de espaço extra. Em seguida, movi esse extra para a base, para que eu sempre tivesse um pouco de textura e não uma escuridão pura.

Então, no meu sombreador (semelhante a um arquivo de efeitos), obtenho a cor do fragmento para a textura e multiplico pela cor de iluminação criada acima. Isso significa que o brilho total fornece a textura quando foi criada. Um brilho muito baixo dá a textura muito escura (mas não preta por causa da cor de base).

EDITAR

Para obter a luz de um rosto, você olha para o cubo na direção normal do rosto. Por exemplo, a face superior do cubo obtém os dados de luz do cubo acima.

EDIT 2

Tentarei abordar algumas de suas perguntas.

Então, o que eu faria é algo como recursão neste caso?

Você pode usar um algoritmo recursivo ou iterativo. Cabe a você como deseja implementá-lo. Apenas certifique-se de acompanhar quais cubos já foram adicionados, caso contrário você continuará para sempre.

Além disso, como o algoritmo "acenderia para baixo"?

Se você está falando sobre luz solar, a luz solar é um pouco diferente, pois não queremos que ela diminua o brilho. Meus dados de cubo incluem um bit SKY. Se um cubo estiver marcado como SKY, isso significa que ele tem acesso claro ao céu aberto acima dele. Os cubos SKY sempre obtêm iluminação completa menos o nível de escuridão. Em seguida, cubos próximos ao céu, que não são o céu, como entradas de cavernas ou saliências, o procedimento normal de iluminação assume o controle. Se você está falando apenas de uma luz brilhando ... é igual a qualquer outra direção.

Como especificaria a luz apenas para um único rosto?

Você não especifica luz apenas para um único rosto. Cada cubo transparente especifica a luz para todos os cubos sólidos que compartilham uma face com ele. Se você deseja obter a luz para um rosto, basta verificar o valor da luz do cubo transparente em que está tocando. Se não estiver tocando em um cubo transparente, você não o renderizará.

Amostras de código?

Não.

MichaelHouse
fonte
@ Byte56 Gostaria de saber como traduzir esse algoritmo para trabalhar com "pedaços".
Gopgop
Pensei em apenas atualizando cada lado do pedaço de acordo com o vizinho e, em seguida, adicionar todos os blocos alterados para a lista dos blocos para a mudança, mas não parecem funcionar
gopgop
@gopgop Os comentários não são o lugar para discussão. Você pode me encontrar no chat algum tempo. Ou discuta com as outras pessoas lá.
MichaelHouse
4

Leia o artigo a seguir, pois ele deve fornecer muitas informações sobre o que você procura. Existem muitas seções sobre iluminação, mas em particular leia as seções sobre Oclusão ambiental, Luz do observador e Coleta de luz:

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

Mas, tentando responder ao cerne da sua pergunta:

  • Se você deseja escurecer uma cor, multiplique -a por outra cor (ou simplesmente por um ponto flutuante entre 0 e 1 se desejar escurecer todos os componentes igualmente), onde 1 em um componente representa intensidade total enquanto 0 representa escuridão total.
  • Se você quiser clarear uma cor, adicione outra cor e prenda o resultado. Mas tome cuidado para não escolher valores tão altos que tornem o resultado saturado em branco puro. Escolha cores escuras, com uma dica do tom que você está procurando, como um laranja escuro (# 886600).

    ou...

    Após adicionar a cor, em vez de fixar o resultado (ou seja, fixar todos os componentes da cor entre 0 e 1), dimensione o resultado - encontre qual dos três componentes (RGB) é o maior e divida todos por esse valor. Este método preserva um pouco melhor as propriedades originais da cor.

David Gouveia
fonte