I wrote a python code that generates both a height map and texture map (see example below) from USGS topographic data. The idea is to create a coding pipeline that allows the user to generate a rendered image of topography anywhere in the world without a whole lot of data wrangling. I do know of the existence of BlenderGIS, which is awesome.
I then take the height and texture map to displace and texture a plane in blender. I saw an example of this workflow in a youtube tutorial a few years ago.
Below shows the heightmap, texture map, example renders from 4.0 and 4.1+, and the shading node trees from 4.0 and 4.1+. To me, it looks like the renders from 4.1+ are applying the basic shadows, but not from Cycles, but instead Eevee? Does anyone else have any ideas what changed from 4.0 to 4.1 that would break this workflow? Thanks in advance.
Height Map Texture Map Result of Render in Blender 4.0 Result of Render in Blender 4.1+ Shading Node Tree in Blender 4.0 Shading Node Tree in Blender 4.1+
Here is a code snippet of the function I used to set up the topography in python. The displacement file is the path to the height map and the color file is the path to the texture map. The dimensions file holds information about the measurements of the topography; it's a simple list of [horizontal x-length, horizontal y-length, height].
def setup_topo(dimensions_file,displacement_file,color_file):
# Create an image from the datafile
displacement_image = bpy.data.images.load(parent + "/" + displacement_file)
color_image = bpy.data.images.load(parent + "/" + color_file)
# make plane
topo_mesh = bpy.ops.mesh.primitive_plane_add(size=plane_size,location=(0.0,0.0,0.0))
bpy.context.active_object.name = 'Topography'
topo_obj = bpy.context.active_object
# Change the shape of the object to match data_aspect
width, height = displacement_image.size
if width / height > 1.0:
topo_obj.scale = (1, height / width, 1)
else:
topo_obj.scale = (width / height, 1, 1)
# dimensions if exists
if dimensions_file != "NULL":
dimensions = np.load(parent + "/" + dimensions_file)
x_length = dimensions[0]
y_length = dimensions[1]
relief = dimensions[2]
else:
x_length = width
y_length = height
relief = 1.0 # if no dimensions given, relief is assumed to be equal to the exaggeration parameter
# add material
topo_mat = bpy.data.materials.new("topo_mat")
topo_mat.cycles.displacement_method = "DISPLACEMENT"
topo_mat.use_nodes = True
# calculate subdivisions
order_of_magnitude = math.floor(math.log10(number_of_subdivisions))
first_digit = int(np.round(number_of_subdivisions / (10.0**order_of_magnitude)))
topo_obj.data.materials.append(topo_mat)
bpy.ops.object.mode_set(mode="EDIT")
for i in range(0, order_of_magnitude):
bpy.ops.mesh.subdivide(number_cuts=10)
bpy.ops.mesh.subdivide(number_cuts=first_digit)
bpy.ops.object.mode_set(mode="OBJECT")
# add image node - determines the displacement
displacement_image_node = topo_mat.node_tree.nodes.new("ShaderNodeTexImage")
# assign png to image to node
displacement_image_node.image = displacement_image
# change colorspace to b&w
displacement_image_node.image.colorspace_settings.name = "sRGB"
# add image node - determines the color of the lanscape
color_image_node = topo_mat.node_tree.nodes.new("ShaderNodeTexImage")
# assign png to image to node
color_image_node.image = color_image
# change colorspace to b&w
color_image_node.image.colorspace_settings.name = "sRGB"
# add displacement node
displacement_node = topo_mat.node_tree.nodes.new("ShaderNodeDisplacement")
displacement_node.inputs.get("Scale").default_value = (
exaggeration * plane_size * relief / max(x_length, y_length)
)
displacement_node.inputs.get("Midlevel").default_value = 0.0
# connect nodes
topo_mat.node_tree.links.new(
displacement_image_node.outputs["Color"], displacement_node.inputs["Height"]
)
topo_mat.node_tree.links.new(
displacement_node.outputs["Displacement"],
topo_mat.node_tree.nodes["Material Output"].inputs["Displacement"],
)
topo_mat.node_tree.links.new(
color_image_node.outputs["Color"],
topo_mat.node_tree.nodes["Principled BSDF"].inputs[0],
)
# change material properties
topo_mat.node_tree.nodes["Principled BSDF"].inputs.get("IOR").default_value = 1.0
topo_mat.node_tree.nodes["Principled BSDF"].inputs.get("Emission Strength").default_value = 0.0
topo_mat.node_tree.nodes["Principled BSDF"].inputs.get("Roughness").default_value = 1.0
# organize nodes
displacement_image_node.location.x, displacement_image_node.location.y = -500, 500
color_image_node.location.x, color_image_node.location.y = -500, 0
displacement_node.location.x, displacement_node.location.y = -200, 500
topo_mat.node_tree.nodes["Principled BSDF"].location.x, topo_mat.node_tree.nodes["Principled BSDF"].location.y = -100, 0
topo_mat.node_tree.nodes["Material Output"].location.x, topo_mat.node_tree.nodes["Material Output"].location.y = 300, 0
return width, height