import bpy from pathlib import Path def bake_heightmap(terrain_obj, resolution=1024, output_path=None): """ Bake terrain heightmap using Blender's render/bake system. Args: terrain_obj: Terrain mesh object resolution: Texture resolution (square) output_path: Path to save EXR file """ print(f"Baking heightmap for: {terrain_obj.name}") print(f"Resolution: {resolution}×{resolution}") # Ensure object has UV map if not terrain_obj.data.uv_layers: print("Adding UV map...") terrain_obj.data.uv_layers.new(name="UVMap") # Create new image for baking bake_image = bpy.data.images.new( name="Heightmap_Bake", width=resolution, height=resolution, alpha=False, float_buffer=True, is_data=True ) # Setup material for baking if not terrain_obj.data.materials: mat = bpy.data.materials.new(name="Heightmap_Material") terrain_obj.data.materials.append(mat) else: mat = terrain_obj.data.materials[0] mat.use_nodes = True nodes = mat.node_tree.nodes nodes.clear() # Create nodes for height baking # Geometry node to get position geo_node = nodes.new(type='ShaderNodeNewGeometry') # Separate XYZ to get Z (height) separate_node = nodes.new(type='ShaderNodeSeparateXYZ') mat.node_tree.links.new(geo_node.outputs['Position'], separate_node.inputs['Vector']) # Emission shader to output height value emission_node = nodes.new(type='ShaderNodeEmission') mat.node_tree.links.new(separate_node.outputs['Z'], emission_node.inputs['Color']) # Material output output_node = nodes.new(type='ShaderNodeOutputMaterial') mat.node_tree.links.new(emission_node.outputs['Emission'], output_node.inputs['Surface']) # Add image texture node (required for baking target) image_node = nodes.new(type='ShaderNodeTexImage') image_node.image = bake_image image_node.select = True nodes.active = image_node # Select object and set mode bpy.context.view_layer.objects.active = terrain_obj terrain_obj.select_set(True) # Setup render settings for baking bpy.context.scene.render.engine = 'CYCLES' bpy.context.scene.cycles.samples = 1 bpy.context.scene.cycles.bake_type = 'EMIT' print("Baking...") bpy.ops.object.bake(type='EMIT', use_clear=True) print("Bake complete!") # Save as EXR if output_path: bake_image.filepath_raw = str(output_path) bake_image.file_format = 'OPEN_EXR' bake_image.use_half_precision = False scene = bpy.context.scene original_color_mode = scene.render.image_settings.color_mode original_color_depth = scene.render.image_settings.color_depth scene.render.image_settings.color_mode = 'BW' scene.render.image_settings.color_depth = '32' bake_image.save_render(str(output_path), scene=scene) scene.render.image_settings.color_mode = original_color_mode scene.render.image_settings.color_depth = original_color_depth print(f"Saved to: {output_path}") # Cleanup bpy.data.images.remove(bake_image) return True if __name__ == "__main__": project_root = Path(bpy.data.filepath).parent.parent output_path = project_root / "textures" / "terrain_heightmap.exr" output_path.parent.mkdir(parents=True, exist_ok=True) # Find terrain object terrain_obj = bpy.data.objects.get("TerrainPlane") if not terrain_obj: print("'TerrainPlane' not found. Searching for terrain mesh...") for obj in bpy.data.objects: if obj.type == 'MESH' and ('terrain' in obj.name.lower() or 'plane' in obj.name.lower()): terrain_obj = obj print(f"Using: {obj.name}") break if not terrain_obj: raise ValueError("No terrain object found!") bake_heightmap( terrain_obj=terrain_obj, resolution=1000, output_path=output_path ) print("\n" + "="*60) print("Heightmap baking complete!") print(f"Output: {output_path}") print("="*60)