Uniday's Procedural Terrain Script

I’m trying to procedurally generate terrains through python as taught in the following tutorial (Brazilian Portuguese):

Unfortunately the component simply doesn’t work right from the beginning. The video link starts at the time the problem occurs to me. Maybe because he is using an older version.

A sample of the code:

#This component fails to generate noise on the plane.

import bge
from collections import OrderedDict
from mathutils import Vector, noise

class Terrain(bge.types.KX_PythonComponent):
    args = OrderedDict([])

    def start(self, args):
        mesh = self.object.meshes[0]

        for index in range(mesh.getVertexArrayLength(0)):
            vertex = mesh.getVertex(0, index)
            pos = vertex.XYZ
            vertex.z = noise.hetero_terrain( pos, 1.0, 200.0, 8, 1.0 ) * 20.0

    def update(self):
        pass

What is the problem with my project?

TL;TR

From @aWeirdOwl .

image

image

UPBGE 0.3+ Solution

  • Use BPY instead.

Preview

Script

import bge, bpy
from collections import OrderedDict
from mathutils import noise

class Terrain(bge.types.KX_PythonComponent):
    args = OrderedDict([])

    def start(self, args):
        mesh = bpy.data.objects[str(self.object.name)].data

        for v in mesh.vertices:
            v.co.z = noise.hetero_terrain(v.co, 1.0, 200.0, 8, 1.0) * 20.0

    def update(self):
        pass

Blend

terrain_rp.blend (2.4 MB)

1 Like

@RPaladin Thank you so much! :trophy:

Now we are trying to improve position with worldTransform so more planes can be put together.

import bge, bpy
from collections import OrderedDict
from mathutils import Vector, noise

class Terrain(bge.types.KX_PythonComponent):
    args = OrderedDict([])

    def start(self, args):
        mesh = bpy.data.objects[str(self.object.name)].data

        for vertex in mesh.vertices:
            position = self.object.worldTransform * vertex.co
            position.z = noise.hetero_terrain(position * 0.02, 1.0, 200.0, 8, 1.0) * 20.0

    def update(self):
        pass

And Blender returns the following error:

Blender Game Engine Started
<class ‘Matrix’>
[ERROR] KX_PythonComponent[Terrain] - Failed to invoke the start callback.
Traceback (most recent call last):
File “D:\gaming\UPBGE\projects\terrain\terrain.py”, line 12, in start
position = self.object.worldTransform * vertex.co
TypeError: Element-wise multiplication: not supported between ‘Matrix’ and ‘Vector’ types
Blender Game Engine Finished
[Logic Nodes][NOTIFICATION] Building Logic Trees on Startup…
Debug: 1586, 851
rcti: : xmin 0, xmax 1585, ymin 100, ymax 950 (1585x850)

I couldn’t find the answer elsewhere. How can it be solved?

Solution Preview

TL;TR

  • Blender 2.8+ requires the asterisk * to be replaced with an at symbol @ for Matrix transforms as mentioned here.
  • Don’t assign vertex location to the position attribute by the way, assign their locations to their actual location attributes vertex.co.z.

Script

import bge, bpy
from collections import OrderedDict
from mathutils import Vector, noise

class Terrain(bge.types.KX_PythonComponent):
    args = OrderedDict([])

    def start(self, args):
        mesh = bpy.data.objects[str(self.object.name)].data

        for vertex in mesh.vertices:
            pos = self.object.worldTransform @ vertex.co
            vertex.co.z = noise.hetero_terrain(Vector(pos * 0.02), 1.0, 200.0, 8, 1.0) * 20.0

    def update(self):
        pass
1 Like

@RPaladin you are helping a lot.

Thanks again!

Glad I could help again. :slightly_smiling_face:
Feel free to share any more problems if you come across any.

1 Like

Why did you use str() and Vector() when they are apparently not necessary?

Good question. The Vector() statement I forgot to remove. :slight_smile:
I used it for testing. Feel free to remove it.

As for the str(), it’s always good practice to use it when using attribute values rather then raw strings, as not doing so can sometimes break the string syntax. It doesn’t always break, but again, occasionally*. You can also use modulus strings and also f-strings to replace the str() if you’d rather not use it.

1 Like

I got stuck again.

Since we replaced KX_MeshProxy with bpy, ‘Mesh’ object has no attribute ‘getVertex’.

import bge
from bpy import data
from collections import OrderedDict
from mathutils import Vector, noise

class Terrain(bge.types.KX_PythonComponent):
    args = OrderedDict([])

    def start(self, args):
        mesh = data.objects[self.object.name].data

        # Generates topography
        for vertex in mesh.vertices:
            pos = vertex.co

            # Turns the position into global to allow for continuity of relief between planes.
            pos = self.object.worldTransform @ pos

            pos /= 50.0 # Scales up noise
            height = noise.hetero_terrain(pos, 1.0, 200.0, 8, 1.0)
            height *= 20.0
            vertex.co.z = height

        # Updates vertex normals (lighting)
        for poly in mesh.polygons:
            v1 = mesh.getVertex(0, poly.v1).XYZ - mesh.getVertex(0, poly.v2).XYZ
            v2 = mesh.getVertex(0, poly.v3).XYZ - mesh.getVertex(0, poly.v2).XYZ

            for vertex in poly.vertices:
                vertex.normal = v2.cross(v1).normalized()

    def update(self):
        pass

Blender Game Engine Started
[ERROR] KX_PythonComponent[Terrain] - Failed to invoke the start callback.
Traceback (most recent call last):
File “D:\UPBGE\…\terrain.py”, line 26, in start
v1 = mesh.getVertex(0, poly.v1).XYZ - mesh.getVertex(0, poly.v2).XYZ
AttributeError: ‘Mesh’ object has no attribute ‘getVertex’
Blender Game Engine Finished

What to do?

For this:

for poly in mesh.polygons:
  v1 = mesh.getVertex(0, poly.v1).XYZ - mesh.getVertex(0, poly.v2).XYZ
  v2 = mesh.getVertex(0, poly.v3).XYZ - mesh.getVertex(0, poly.v2).XYZ

I think this:

o = data.objects[self.object.name]
for vertex in o.data.vertices:
  v1 = o.matrix_world @ o.data.vertices[0].co - o.matrix_world @ o.data.vertices[1].co
  v2 = o.matrix_world @ o.data.vertices[2].co - o.matrix_world @ o.data.vertices[1].co

As for this:

for vertex in poly.vertices:
  vertex.normal = v2.cross(v1).normalized()

It’s a bit out of my league. Sorry. The best I could come up with at the moment was the code below, but it doesn’t work and raises an error:

import bge, bpy, mathutils
from collections import OrderedDict

class Terrain(bge.types.KX_PythonComponent):
    args = OrderedDict([])

    def start(self, args):
        
        o = bpy.data.objects[self.object.name]
        o.data.use_auto_smooth = True
        o.data.normals_split_custom_set([(0, 0, 0) for l in o.data.loops])
        
        normals = []
        
        for vertex in o.data.vertices:
            pos = vertex.co
            pos = self.object.worldTransform @ pos
            pos /= 50.0
            height = mathutils.noise.hetero_terrain(pos, 1.0, 200.0, 8, 1.0)
            height *= 20.0
            vertex.co.z = height

        for poly in o.data.polygons:
            for vertex in o.data.vertices:
                v1 = o.matrix_world @ o.data.vertices[0].co - o.matrix_world @ o.data.vertices[1].co
                v2 = o.matrix_world @ o.data.vertices[2].co - o.matrix_world @ o.data.vertices[1].co
                normals.append(vertex.normal)
                
        o.data.normals_split_custom_set_from_vertices(normals)

    def update(self):
        pass

Raised error:

RuntimeError: Error: Number of custom normals is not number of vertices (73984.000000 / 289)

Edit: I think because it’s required for normal changes to be done in edit-mode, but that’s not exactly possible at runtime I believe. I could be wrong.

1 Like