Texture mapping according with vertex position(terrain generation)

Hello,

I’m trying to write a simple terrain generator. Insofar, I’ve been able to generate a model but now I’m struggling with texture mapping. What I would like to accomplish is to assign(and map) different textures to vertices according with their position. Do you know if there exist any enlightening documentation piece about this?

Are you attempting to do this in shader, or in Python?

In the former case, it should just be a matter of taking the vertex position (which the Panda should provide to the shader) and using it where you would otherwise use the texture-coordinates.

In the latter case, perhaps look at the manual’s page on “automatic texture coordinates”.

Hello, thanks for your reply.

I’m doing this in Python and I have already read the mentioned page. The problem is thtat it talks about mapping a single texture to a model, what I’m trying to do is to apply different textures according with vertex positions, in particular, I was wondering if something similar is feasible using texture stages, i.e. if I assign two textures to 2 different texture stages, is there any way to set the level of visibility of such stages?

Ah, I missed that you wanted to map different textures–my apologies!

Hmm… Given that this is for terrain generation, could you not just use separate meshes for separate textures?

Otherwise, hmm, I’m not sure… Perhaps you could assign specific colours to your vertices according to their positions, and then use those with texture combine modes to determine which textures are shown?

But perhaps there’s a better way!

(Perhaps using multiple texture coordinate sets?)

I’ve not done something quite like this myself (I usually use shaders to apply multiple textures), so I’m afraid that I’m somewhat uncertain as to what to recommend. If the above doesn’t help, perhaps someone else will have a better idea!

What exactly did you have in mind for determining which texture is shown? “according with vertex positions” is a little vague.

An oft-used technique is to use a “splat map” texture stretched across the entire terrain which contains information about which texture to show at which position. Some basic splatting can be implemented using texture combine modes, more advanced uses require a custom shader.

1 Like

Hello,

thank you both for your answers. What I have in mind is to, for example, have a basic ground texture(let’s say grass texture) and maybe stone texture and snow texture to insert on slopes and higher places respectively. I’ll look into this splat map technique.

1 Like

Aaah–I was imagining that you wanted different textures for different horizontal regions–grassland here, rocks there, that sort of thing.

Yes, for what you have in mind, a splat map should work pretty well.

That said, it might be simpler to just use a shader that applies the vertical position of a given vertex, perhaps with some perturbation from a texture (e.g. holding Perlin noise), and uses that to determine how to render the textures.

Ok, thanks. Any suggestion on where to start? Moreover, I’m using simplepbr at the moment. Could this be a problem?

I’m afraid that I don’t use simplepbr myself, so I don’t know.

As to where to start–if simplepbr doesn’t prevent your taking this route–I might suggest looking for a tutorial that covers the basics of GLSL, and then building upon that. (And of course asking in the forum if you get stuck!)

Hello,

sorry to bother again with this. I think I made progress but I’m stuck again. I found, in another post, this. I tried to copy textures and shaders:

Vertex

[terrain.vert]

#version 120

uniform float tilingFactor;

varying vec4 normal;

void main()
{
    normal.xyz = normalize(gl_NormalMatrix * gl_Normal);
    normal.w = gl_Vertex.y;

    gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
    gl_TexCoord[0] = gl_MultiTexCoord0 * tilingFactor;
}

Fragment

[terrain.frag]

#version 120

struct TerrainRegion
{
    float min;
    float max;
};

uniform TerrainRegion region1;
uniform TerrainRegion region2;
uniform TerrainRegion region3;
uniform TerrainRegion region4;

uniform sampler2D region1ColorMap;
uniform sampler2D region2ColorMap;
uniform sampler2D region3ColorMap;
uniform sampler2D region4ColorMap;

varying vec4 normal;

vec4 GenerateTerrainColor()
{
    vec4 terrainColor = vec4(0.0, 0.0, 0.0, 1.0);
    float height = normal.w;
    float regionMin = 0.0;
    float regionMax = 0.0;
    float regionRange = 0.0;
    float regionWeight = 0.0;

    // Terrain region 1.
    regionMin = region1.min;
    regionMax = region1.max;
    regionRange = regionMax - regionMin;
    regionWeight = (regionRange - abs(height - regionMax)) / regionRange;
    regionWeight = max(0.0, regionWeight);
    terrainColor += regionWeight * texture2D(region1ColorMap, gl_TexCoord[0].st);

    // Terrain region 2.
    regionMin = region2.min;
    regionMax = region2.max;
    regionRange = regionMax - regionMin;
    regionWeight = (regionRange - abs(height - regionMax)) / regionRange;
    regionWeight = max(0.0, regionWeight);
    terrainColor += regionWeight * texture2D(region2ColorMap, gl_TexCoord[0].st);

    // Terrain region 3.
    regionMin = region3.min;
    regionMax = region3.max;
    regionRange = regionMax - regionMin;
    regionWeight = (regionRange - abs(height - regionMax)) / regionRange;
    regionWeight = max(0.0, regionWeight);
    terrainColor += regionWeight * texture2D(region3ColorMap, gl_TexCoord[0].st);

    // Terrain region 4.
    regionMin = region4.min;
    regionMax = region4.max;
    regionRange = regionMax - regionMin;
    regionWeight = (regionRange - abs(height - regionMax)) / regionRange;
    regionWeight = max(0.0, regionWeight);
    terrainColor += regionWeight * texture2D(region4ColorMap, gl_TexCoord[0].st);

    return terrainColor;
}

void main()
{
    vec3 n = normalize(normal.xyz);

    float nDotL = max(0.0, dot(n, gl_LightSource[0].position.xyz));

    vec4 ambient = gl_FrontLightProduct[0].ambient;
    vec4 diffuse = gl_FrontLightProduct[0].diffuse * nDotL;
    vec4 color = gl_FrontLightModelProduct.sceneColor + ambient + diffuse;

    gl_FragColor = color * GenerateTerrainColor();
}

then I tried to apply them in my situation(without simplepbr):

        gnd_texture = loader.loadTexture('textures/grass.JPG')
        snow_texture = loader.loadTexture('textures/snow.JPG')
        ts1 = TextureStage('gnd1')
        ts2 = TextureStage('gnd2')

        ts1.setSort(1)
        ts2.setSort(2)
        self.tRoot.setTexture(ts1,gnd_texture)
        self.tRoot.setTexture(ts2,snow_texture)
        shader = Shader.load(vertex='shaders/terrain.vert',
                             fragment='shaders/terrain.frag',
                             lang=Shader.SL_GLSL)
        self.tRoot.setShader(shader)

what I obtain is just the grass texture. Should I set the texture stages in some way or I’m simply misunderstanding the shaders’ purpose?

Are you perhaps seeing errors in your console/terminal?

Looking at that shader, it seems to be expecting rather more shader-inputs than you’re actually providing. As a result, I’m wondering whether it isn’t failing entirely, and Panda thus falling back on default rendering for the terrain. This might then show just a single texture, or some relatively-simple form of blending–hence your seeing just grass.

(To be clear, it looks like that shader should do what you want–it’s just expecting certain control-values (not to mention more than two input textures) that aren’t being set in your code, that I see.)

Hey, thanks again for your input. I tried to define the variables according with the original source code, now it displays everything but I simply get the four textures displayed one on top of the other. Should I set the TextureStages’ modes? Could it be an issue with UV or normals?

BTW, here is the code:

[TerrainRegion class and HEIGHTMAP_SCALE]

class TerrainRegion():
    def __init__(self,minimum,maximum,texture,textureObject):
        self.minimum = minimum
        self.maximum = maximum
        self.texture = texture
        self.textureObject = textureObject

HEIGHTMAP_SCALE = 2.0

[updated code]

        self.terrainRegions = [
                    TerrainGenerator.TerrainRegion(0.0, 50.0 * HEIGHTMAP_SCALE, 0, loader.loadTexture("GLSL_TUTORIAL/content/textures/dirt.JPG")),
                    TerrainGenerator.TerrainRegion(51.0 * HEIGHTMAP_SCALE, 101.0 * HEIGHTMAP_SCALE, 0, loader.loadTexture("GLSL_TUTORIAL/content/textu
res/grass.JPG")),
                    TerrainGenerator.TerrainRegion(102.0 * HEIGHTMAP_SCALE, 203.0 * HEIGHTMAP_SCALE, 0, loader.loadTexture("GLSL_TUTORIAL/content/text
ures/rock.JPG")),
                    TerrainGenerator.TerrainRegion(204.0 * HEIGHTMAP_SCALE, 255.0 * HEIGHTMAP_SCALE, 0, loader.loadTexture("GLSL_TUTORIAL/content/text
ures/snow.JPG"))
                ]

        ts1 = TextureStage('gnd1')
        ts2 = TextureStage('gnd2')
        ts3 = TextureStage('gnd3')
        ts4 = TextureStage('gnd4')

        ts1.setSort(0)
        ts2.setSort(1)
        ts3.setSort(2)
        ts4.setSort(3)

        self.tRoot.setTexture(ts1,self.terrainRegions[0].textureObject)
        self.tRoot.setTexture(ts2,self.terrainRegions[1].textureObject)
        self.tRoot.setTexture(ts3,self.terrainRegions[2].textureObject)
        self.tRoot.setTexture(ts4,self.terrainRegions[3].textureObject)

        shader = Shader.load(vertex='GLSL_TUTORIAL/content/shaders/terrain.vert',fragment='GLSL_TUTORIAL/content/shaders/terrain.frag',lang=Shader.SL_GLSL)

        self.tRoot.setShaderInput("tilingFactor",HEIGHTMAP_SCALE)
        self.tRoot.setShaderInput("region1.max",self.terrainRegions[0].maximum)
        self.tRoot.setShaderInput("region1.min",self.terrainRegions[0].minimum)
        self.tRoot.setShaderInput("region2.max",self.terrainRegions[1].maximum)
        self.tRoot.setShaderInput("region2.min",self.terrainRegions[1].minimum)
        self.tRoot.setShaderInput("region3.max",self.terrainRegions[2].maximum)
        self.tRoot.setShaderInput("region3.min",self.terrainRegions[2].minimum)
        self.tRoot.setShaderInput("region4.max",self.terrainRegions[3].maximum)
        self.tRoot.setShaderInput("region4.min",self.terrainRegions[3].minimum)

        self.tRoot.setShaderInput("region1ColorMap",0)
        self.tRoot.setShaderInput("region2ColorMap",1)
        self.tRoot.setShaderInput("region3ColorMap",2)
        self.tRoot.setShaderInput("region4ColorMap",3)

        self.tRoot.setShader(shader)



        self.terrain.update()



here is a screenshot:

and here is the original source code(the TerrainRegion struct and the part in which it sets the variables):

    [TerrainRegion struct]
    struct TerrainRegion
    {
        float min;
        float max;
        GLuint texture;
        std::string filename;
    };

    [var setup]
    handle = glGetUniformLocation(g_terrainShader, "tilingFactor");
    glUniform1f(handle, HEIGHTMAP_TILING_FACTOR);

    // Update terrain region 1.

    handle = glGetUniformLocation(g_terrainShader, "region1.max");
    glUniform1f(handle, g_regions[0].max);

    handle = glGetUniformLocation(g_terrainShader, "region1.min");
    glUniform1f(handle, g_regions[0].min);

    // Update terrain region 2.

    handle = glGetUniformLocation(g_terrainShader, "region2.max");
    glUniform1f(handle, g_regions[1].max);

    handle = glGetUniformLocation(g_terrainShader, "region2.min");
    glUniform1f(handle, g_regions[1].min);

    // Update terrain region 3.

    handle = glGetUniformLocation(g_terrainShader, "region3.max");
    glUniform1f(handle, g_regions[2].max);

    handle = glGetUniformLocation(g_terrainShader, "region3.min");
    glUniform1f(handle, g_regions[2].min);

    // Update terrain region 4.

    handle = glGetUniformLocation(g_terrainShader, "region4.max");
    glUniform1f(handle, g_regions[3].max);

    handle = glGetUniformLocation(g_terrainShader, "region4.min");
    glUniform1f(handle, g_regions[3].min);

    // Bind textures.

    glUniform1i(glGetUniformLocation(g_terrainShader, "region1ColorMap"), 0);
    glUniform1i(glGetUniformLocation(g_terrainShader, "region2ColorMap"), 1);
    glUniform1i(glGetUniformLocation(g_terrainShader, "region3ColorMap"), 2);
    glUniform1i(glGetUniformLocation(g_terrainShader, "region4ColorMap"), 3);

Hmm… What I’d suggest is doing some testing to see where the problem might be coming from. Offhand, if the textures are all mixing together, that might indicate that they’re all ending up with the same “regionWeight”. So, the first test that I might suggest is to change the shader such that it outputs the first three “regionWeights” as red, green, and blue values (i.e. overriding the actual texture values for testing purposes).

It should be possible to do this by storing each “regionWeight” in a separate variable as its calculated, and then adding a line that sets the value of “terrainColor” to have the three “regionWeight” values as its components.

(This is why I’m suggesting that you only output the first three “regionWeights”, despite there being four: there are only three components to an output colour, excluding the alpha-value.)

If all is working as expected, you should see your terrain with the three colours shading into each other. If, as I suspect, you’re getting the same weights everywhere, you should see a uniform mix of the colours.

This is wrong, and I’m surprised that it didn’t produce an error message:

Panda takes care of the binding locations for you, you should just specify the textures directly:

self.tRoot.setShaderInput("region1ColorMap", self.terrainRegions[0].textureObject)

If you wanted to use setTexture to specify the textures, you would need to change the shader to use the pre-defined names p3d_Texture0, p3d_Texture1, etc, which map to the ordered texture stages.

Other than that, gl_FrontLightProduct et al is some very arcane and ancient GLSL, I can’t promise that that still works nowadays! If your GLSL tutorial is teaching you that, I do suggest you find a more up-to-date tutorial… one from 2009 or later should work better!

1 Like

Hello,

thanks again for your answers. I think I have made some progress :slight_smile:

first of all, here are the shaders(sorry, there are plenty of non-necessary variables, still have to do the cleanup) and the results:

#version 150

//Vertex


// Uniform inputs
uniform mat4 p3d_ModelViewProjectionMatrix;

// Vertex inputs
in vec4 p3d_Vertex;
in vec4 p3d_Normal;
in vec2 p3d_MultiTexCoord0;
in vec2 p3d_MultiTexCoord1;
in vec2 p3d_MultiTexCoord2;
in vec2 p3d_MultiTexCoord3;

// Output to fragment shader
out vec2 texcoord0;
out vec2 texcoord1;
out vec2 texcoord2;
out vec2 texcoord3;

varying vec4 vertex;

void main() {
  gl_Position = p3d_ModelViewProjectionMatrix * p3d_Vertex;
  texcoord0 = p3d_MultiTexCoord0;
  texcoord1 = p3d_MultiTexCoord1;
  texcoord2 = p3d_MultiTexCoord2;
  texcoord3 = p3d_MultiTexCoord3;
  vertex = p3d_Vertex;
}
#version 150

//Fragment


uniform sampler2D p3d_Texture0;
uniform sampler2D p3d_Texture1;
uniform sampler2D p3d_Texture2;
uniform sampler2D p3d_Texture3;
uniform sampler2D Heightmap;

// Input from vertex shader
in vec2 texcoord0;
in vec2 texcoord1;
in vec2 texcoord2;
in vec2 texcoord3;

in vec4 vertex;

// Output to the screen
out vec4 p3d_FragColor;

void main() {
  vec4 color = texture(p3d_Texture2, texcoord2);
  //p3d_FragColor = color.rgba;
  vec4 texel0 = texture2D(p3d_Texture0, texcoord0.st).rgba;
  vec4 texel1 = texture2D(p3d_Texture1, texcoord1.st).rgba;
  vec4 texel2 = texture2D(p3d_Texture2, texcoord2.st).rgba;
  vec4 texel3 = texture2D(p3d_Texture3, texcoord3.st).rgba;
  vec4 heightMapTexel = texture2D(Heightmap,texcoord0.st);

  p3d_FragColor = mix(texel1,texel3,heightMapTexel.r);
}

screenshot:

as you can see, I’m just mixing texture1(grass) and texture3(snow). What I would like to do is to insert also texture2(rock) on the slopes. What I was thinking about is if there is a way to use mix in this case too: mix(mix(texel1,texel3,heightmapTexel.r),texture2,someParameter). The problem is that I don’t know what parameter use :smiley: . Any suggestion on where to start?

Well, extending the approach that you’re currently using, you could use another colour-channel: you’re currently just using the red channel (heightMapTexel.r); you could additionally use the green channel or the blue channel. (Or, if you want to, the alpha channel.)

The problem is that I don’t know how to check if there is a slope or not :-/

Well, if you have a heightmap, then you could paint it in manually.

Otherwise, you could use a bit of maths to compare neighbouring heightmap samples and so determine a local slope; this could perhaps be done once per heightmap, rather than in the shader, and the result stored in, say, the green channel.

Yep, that could be an idea. I’ll try to do so. Thanks :slight_smile:

1 Like

Ok, I managed to fix things up using GeoMipTerrain’s makeSlopeImage method. This will be the last thing I will ask(I promise :smiley: ).

I’m trying to rescale the textures using setTexScale but it doesn’t seem to work: What I get is the texture stretched on the whole model. What am I doing wrong?

here is the code:

 31     tRoot = terrain.getRoot()
 32     ts_idx = 0
 33     for tex in textures:
 34         ts = TextureStage('gnd' + str(ts_idx))
 35         ts.setSort(ts_idx)
 36         texture = loader.loadTexture(tex)
 37         tRoot.setTexture(ts,texture)
 38         tRoot.setTexScale(ts,1000,1000)
 39         ts_idx+=1

1 Like