Zylann / godot_voxel

Voxel module for Godot Engine
MIT License
2.59k stars 244 forks source link

multiple textures for smooth terrain #93

Open blockspacer opened 4 years ago

blockspacer commented 4 years ago

I`m trying to add support for multiple textures based on texarray and COLOR attributes of vertices.

image

use a splat/texarray shader and then all you need is to populate the COLOR attributes of vertices with the material ID, which you use in shader https://github.com/Zylann/godot_voxel/issues/83#issuecomment-552124059

Key idea is to texture differently shape created by do_point or do_sphere. For example, it may be useful in games where player can place voxel with chosen texture https://youtu.be/Xm1KVjMePBA?t=85

I managed to pass texture ID via _output_extra from voxel_mesher_transvoxel.cpp and adding CHANNEL_DATA2 to channels_mask like so unsigned int channels_mask = (1 << VoxelBuffer::CHANNEL_SDF) | (1 << VoxelBuffer::CHANNEL_DATA2);

But i don't know which element to choose from corner_positions to use in get_voxel (voxel_mesher_transvoxel.cpp)

                corner_positions[0] = Vector3i(pos.x, pos.y, pos.z);
                corner_positions[1] = Vector3i(pos.x + 1, pos.y, pos.z);
                corner_positions[2] = Vector3i(pos.x, pos.y + 1, pos.z);
                corner_positions[3] = Vector3i(pos.x + 1, pos.y + 1, pos.z);
                corner_positions[4] = Vector3i(pos.x, pos.y, pos.z + 1);
                corner_positions[5] = Vector3i(pos.x + 1, pos.y, pos.z + 1);
                corner_positions[6] = Vector3i(pos.x, pos.y + 1, pos.z + 1);
                corner_positions[7] = Vector3i(pos.x + 1, pos.y + 1, pos.z + 1);

Now i'm using corner_positions[0]

texture_idx = (float)voxels.get_voxel(corner_positions[0], VoxelBuffer::CHANNEL_DATA2);

But corner_positions[0] seems related not to voxel, but to voxel nearby to it. That's why when i call do_point texture applies not to created voxel (but to part of mesh created by do_point).

Thanks in advance for any help provided.

Zylann commented 4 years ago

You cannot use COLOR with the Transvoxel mesher at the moment, because it is used already to store transition mesh information.

Transvoxel polygonizes a block by taking corners of a cube (which is why it uses 8 voxels instead of one), which are interpolated to generate vertices. Then the process repeats for every voxel of the block ("marching" cubes). You are right about using a new channel for material IDs, but doing that in Transvoxel you need to make them part of the interpolation I guess. Also, the paper has a section dedicated to this subject, you may want to take a look: https://transvoxel.org/Lengyel-VoxelTerrain.pdf

blockspacer commented 4 years ago

@Zylann Thanks for response.

I'll try to use UV2 and pack multiple texture ids into one float like so https://stackoverflow.com/questions/32915724/pack-two-floats-within-range-into-one-float.

About COLOR - are there any usage examples with shaders and Transvoxel? I changed triplanar shader to use get_transvoxel_position from https://github.com/Zylann/godot_voxel/issues/2#issuecomment-570013045 and nothing changed.

Zylann commented 4 years ago

This is the shader I use in my local demo:

shader_type spatial;

uniform sampler2D u_texture_top : hint_albedo;
uniform sampler2D u_texture_sides : hint_albedo;
uniform int u_transition_mask;

varying vec3 v_world_pos;
varying vec3 v_world_normal;

vec3 get_triplanar_blend(vec3 world_normal) {
    vec3 blending = abs(world_normal);
    blending = normalize(max(blending, vec3(0.00001))); // Force weights to sum to 1.0
    float b = blending.x + blending.y + blending.z;
    return blending / vec3(b, b, b);
}

vec4 texture_triplanar(sampler2D tex, vec3 world_pos, vec3 blend) {
    vec4 xaxis = texture(tex, world_pos.yz);
    vec4 yaxis = texture(tex, world_pos.xz);
    vec4 zaxis = texture(tex, world_pos.xy);
    // blend the results of the 3 planar projections.
    return xaxis * blend.x + yaxis * blend.y + zaxis * blend.z;
}

vec3 get_transvoxel_position(vec3 vertex_pos, vec4 vertex_col) {

    int border_mask = int(vertex_col.a);
    int cell_border_mask = border_mask & 63; // Which sides the cell is touching
    int vertex_border_mask = (border_mask >> 6) & 63; // Which sides the vertex is touching

    // If the vertex is near a side where there is a low-resolution neighbor,
    // move it to secondary position
    int m = u_transition_mask & (cell_border_mask & 63);
    float t = float(m != 0);

    // If the vertex lies on one or more sides, and at least one side has no low-resolution neighbor,
    // don't move the vertex.
    t *= float((vertex_border_mask & ~u_transition_mask) == 0);

    // Position to use when border mask matches
    vec3 secondary_position = vertex_col.rgb;
    return mix(vertex_pos, secondary_position, t);
}

void vertex() {
    VERTEX = get_transvoxel_position(VERTEX, COLOR);
    vec3 world_pos = (WORLD_MATRIX * vec4(VERTEX, 1.0)).xyz;
    v_world_pos = world_pos;
    v_world_normal = NORMAL;
}

void fragment() {
    vec3 normal = v_world_normal;//normalize(v_world_normal);
    vec3 wpos = v_world_pos * 0.2;
    vec3 blending = get_triplanar_blend(normal);
    vec3 top_col = texture_triplanar(u_texture_top, wpos, blending).rgb;
    vec3 side_col = texture_triplanar(u_texture_sides, wpos, blending).rgb;
    float r = top_col.r;
    ALBEDO = mix(side_col, top_col, clamp(normal.y * 10.0 - 4.0 - 8.0*r, 0.0, 1.0));
}
blockspacer commented 4 years ago

@Zylann

I'm not sure that understood doing that in Transvoxel you need to make them part of the interpolation I guess.

I hope that you said about VoxelMesherTransvoxel::build_internal cause i tried to use cell_positions like so https://github.com/blockspacer/godot_voxel/blob/indev/meshers/transvoxel/voxel_mesher_transvoxel.cpp#L873

            const int cornerMaterials[8] =
            {
                fvoxels.get_voxel(cell_positions[0], VoxelBuffer::CHANNEL_DATA2),
                fvoxels.get_voxel(cell_positions[1], VoxelBuffer::CHANNEL_DATA2),
                fvoxels.get_voxel(cell_positions[2], VoxelBuffer::CHANNEL_DATA2),
                fvoxels.get_voxel(cell_positions[3], VoxelBuffer::CHANNEL_DATA2),
                fvoxels.get_voxel(cell_positions[4], VoxelBuffer::CHANNEL_DATA2),
                fvoxels.get_voxel(cell_positions[5], VoxelBuffer::CHANNEL_DATA2),
                fvoxels.get_voxel(cell_positions[6], VoxelBuffer::CHANNEL_DATA2),
                fvoxels.get_voxel(cell_positions[7], VoxelBuffer::CHANNEL_DATA2),
            };
            dominant_texture_idx = (float)FindDominantMaterial(cornerMaterials);

I'm going to use function similar to FindDominantMaterial from https://github.com/nickgildea/leven/blob/118bf1cacb75fdb3b306ccf27477037720af86e8/leven/cl/octree.cl#L96

Note that noise function must mark air voxels with empty texture (ID == 0 or - in my code draft - if(texture_idx < 0.9), so i had to change voxel_stream_noise.cpp like so https://github.com/blockspacer/godot_voxel/blob/indev/streams/voxel_stream_noise.cpp#L87 cause i don't know how determine air voxels from float d = (n + 2.0 * t - 1.0) * iso_scale;

blockspacer commented 4 years ago

BTW, is generation prioritized by distance to the camera?

Zylann commented 4 years ago

Note that noise function must mark air voxels with empty texture (ID == 0 or - in my code draft - if(texture_idx < 0.9), so i had to change voxel_stream_noise.cpp like so https://github.com/blockspacer/godot_voxel/blob/indev/streams/voxel_stream_noise.cpp#L87 cause i don't know how determine air voxels from float d = (n + 2.0 t - 1.0) iso_scale;

You can tell if a voxel is air if its SDF value is above zero (though with Transvoxel it would be the opposite). Then a cell is all air if the 8 corner voxels are air. I can't really tell what to do for materials, all I can say is that I would follow the Transvoxel paper.

BTW, is generation prioritized by distance to the camera?

The order of blocks scheduled for processing is sorted by distance to viewer. However scheduling is controlled by octrees and regions. See #87

blockspacer commented 4 years ago

from https://transvoxel.org/Lengyel-VoxelTerrain.pdf If a vertex is reused from a preceding cell, and the preceding cell selects a different material, then the vertex must be duplicated so that the different texture map indexes can be assigned to it.

Any ideas how to do that will be useful

Zylann commented 3 years ago

FYI I've been working in a branch with texturing support for a little while, in smooth_texturing.

The way it works is as follows:

Two new channels were added, INDICES and WEIGHTS. They both default to 16-bit depth and should remain in that format in order to use the feature. Indices allow to tell which textures are present in a voxel, and weights tell how much of these textures there is. There can be just one (in which case only one weight will be maxed and others zero), or multiples. Indices are encoded as four 4-bit values, and weights are also encoded as four 4-bit values. That means up to 16 different textures are supported, and weights can have up to 16 shades. This is very tight, but it should be enough for now.

The only mesher supporting this is VoxelMesherTransvoxel. It has an option to turn texturing on (it has a performance cost). During meshing, for every voxel crossing the surface, indices and weights are looked up, and 4 textures with the highest amount are selected for generated vertices. If the set of textures changes between two cells, a seam is created to fix interpolation issues that could arise from vertex sharing.

Finally, the shader used to render the surface can read indices and weights in the UV variable. It has nothing to do with uvs, it is only being repurposed. UV.x contains 4 bytes for the 4 indices, and same for weights in UV.y. After some decoding with floatBitsToUint, we can now perform simple 4-way blending using a texture array.

2021_04_25_1848

I then implemented ways to paint textures with VoxelTool. It's much more involved than sculpting, but it works too: 2021_04_25_1846_first_working_smooth_painting

Allowing to generate textures in VoxelGeneratorGraph is tricky as well, but I got something basic working. I had to implement new kinds of outputs (which is groundwork for custom outputs), so the density of each texture can be specified. However it involves math so that's another tricky part, as densities must add up to 1. Performance is not as good as I would like, but it works:

generator_textured

Now what's essentially left to do in the branch is to implement basic texture painting for remaining VoxelTools. Even though this took lots of work, it is still quite basic so it will probably be improved or changed over time.

Zylann commented 3 years ago

The feature is now in master.