Denne veiledningen viser hvordan du klargjør dybdedata for videre bruk i 3D-visualisering, for eksempel i Blender eller webbaserte løsninger. Arbeidsflyten tar utgangspunkt i rå dybdepunkter og optimaliserer datasettet gjennom filtrering, generalisering og konvertering til et format som egner seg for bruk i Blender.
Se også tidligere relevante veiledninger:
Grunnleggende
Før du starter, må du hente dybdepunkt. I dette tilfellet bruker vi Kartverkets WFS-tjeneste: Sjøkart – dybdedata WFS
Du må også lage en utstrekning (extent) over området. Dette kan gjøres enten ved bruk av et shapefile-lag eller et grid i QGIS. Eksporter deretter ønsket dybdepumkt området til en GeoPackage.

Steg 1 – Eksporter lag
- Høyreklikk på dybdepunkter-laget
- Velg Export → Save Features As…
- Sett følgende:
- Format: GeoPackage
- Filnavn: Valgfri plassering
- CRS: Kontroller at den er korrekt
- Under Fields to export:
- Fjern alle felt
- Behold kun:
- ID
- dybde
- Aktiver:
- Include Z-dimension
- Sett extent «Calculate from layer»



Klikk OK
Anbefalt: Når dybdepunktene er ferdig eksportert, kan du slette WFS laget for dybdepunkt. Dette anbefales for å unngå forvirring videre i arbeidet.

Steg 2 – Opprett etikettfelt
- Høyreklikk laget → Open Attribute Table
- Åpne Field Calculator
- Opprett nytt felt:
- Navn:
label - Type: Text (string)
- Navn:
- Bruk uttrykket (expression):
"id" || ' – ' || round("dybde", 1) || ' m'
Klikk OK også Lagre endringer



Steg 3 – Lag grid baserte felt
Etter steg 2 har vi ofte for mange punkter, noe som gjør datasettet rotete og tungt å jobbe med.
Ved å dele området inn i et rutenett (grid) kan vi redusere antall punkter på en kontrollert måte, samtidig som vi bevarer de viktigste dybdedataene.

Åpne Field Calculator igjen og opprett:
1. cell_x
- Type: Integer (32bit)
- Uttrykk:
floor($x / 200)
2. cell_y
- Type: Integer (32bit)
- Uttrykk:
floor($y / 200)
3. cell_id
- Type: Text (string)
- Uttrykk:
floor($x / 200) || '_' || floor($y / 200)




Klikk Update All og lagre.
Fjern duplikater
- Åpne Processing Toolbox
- Søk etter:
- Delete duplicates by attribute
- Sett:
- Input layer: ditt lag
- Attribute:
cell_id


Klikk Run
Resultatet er betydelig redusert rot og bedre oversikt.


Steg 4 – Multipart til singlepart
- Åpne Processing Toolbox
- Søk: Multipart to Singleparts
- Velg laget
- Klikk Run

Steg 5 – Konverter geometri (legg til Z-verdi)
- Søk etter: Geometry by expression
- Sett:
- Geometry type: Point
- ✅ Enable Z dimension
- Bruk uttrykket (Expression):
make_point($x, $y, -"dybde")
Klikk Run

Steg 6 – Eksporter sluttresultat
- Høyre klikk laget, eksport – Save Features As..
- Eksporter laget som Shapefile
- Fields to export:
- id (valgfritt)
- dybde
- label
- Extent: Calculate from layer
- Viktig: IKKE inkluder Z-dimensjon i eksporten

Steg 7 – Import i Blender
- Importer shapefilen via BlenderGIS plugin
- Klikk Separate Objects
- I Shapelayer Collection:
- Velg alle objekter



Steg 8 – Scripting workspace
Nå som punktene er verifisert og følger terrenget korrekt, er det på tide å gi etikettene dybde (3D-tekst) slik at de blir tydelige i scenen. For dette har vi laget et script som kan kjøres i Blender for å automatisere prosessen.
Scriptet inneholder to viktige konfigurasjoner:
CONFIG = {
"MODE": "BACK", # FRONT or BACK
"SHAPEFILE_COLLECTION": "seabed-texture_v2", # Velg modellnavnet etikettene skal projiseres på (terreng + collision rules)
- MODE»: «BACK – lager etiketter som er synlige bakfra.
Kjør også medFRONTfor å gjøre dem synlige fra begge sider (husk å markere punktene på nytt). - SHAPEFILE_COLLECTION – velg modellen etikettene projiseres på.
- Viktig for at de følger terrenget riktig og unngår feil plassering.
- Lim inn scripted nedenfor.
- Kjør koden to ganger:
- Først:
BACK - Deretter:
FRONT
- Først:
Phyton script:
import bpy
import mathutils
# =========================
# CONFIG
# =========================
CONFIG = {
"MODE": "FRONT", # FRONT or BACK
"TEXT_SIZE": 6.0,
"DOT_SIZE": 0.25,
"HEIGHT_OFFSET": 3.0,
"SURFACE_OFFSET": 0.05,
"SHAPEFILE_COLLECTION": "seabed-texture_v2", # Velg modellnavnet etikettene skal projiseres på (terreng + collision rules)
"UPWARD_NORMAL_MIN_Z": 0.3
}
MODE = CONFIG["MODE"]
TEXT_SIZE = CONFIG["TEXT_SIZE"]
DOT_SIZE = CONFIG["DOT_SIZE"]
HEIGHT_OFFSET = CONFIG["HEIGHT_OFFSET"]
SURFACE_OFFSET = CONFIG["SURFACE_OFFSET"]
SHAPEFILE_COLLECTION = CONFIG["SHAPEFILE_COLLECTION"]
UPWARD_NORMAL_MIN_Z = CONFIG["UPWARD_NORMAL_MIN_Z"]
# =========================
# MATERIAL (KEY PART)
# =========================
def get_material(name, color, emission, alpha=1.0):
mat = bpy.data.materials.get(name)
if not mat:
mat = bpy.data.materials.new(name)
mat.use_nodes = True
bsdf = mat.node_tree.nodes["Principled BSDF"]
bsdf.inputs["Base Color"].default_value = color
bsdf.inputs["Emission Strength"].default_value = emission
bsdf.inputs["Alpha"].default_value = alpha
mat.blend_method = 'BLEND'
mat.use_backface_culling = True # 🔥 REQUIRED
return mat
# 🔥 IMPORTANT: separate materials
if MODE == "FRONT":
text_mat = get_material("DepthTextMat_FRONT", (1,1,1,1), 0.2, 0.9)
else:
text_mat = get_material("DepthTextMat_BACK", (1,1,1,1), 0.2, 0.9)
dot_mat = get_material("DepthDotMat", (1,1,1,1), 0.4, 1)
# =========================
# TERRAIN
# =========================
terrain_obj = bpy.data.objects[SHAPEFILE_COLLECTION]
depsgraph = bpy.context.evaluated_depsgraph_get()
terrain_eval = terrain_obj.evaluated_get(depsgraph)
terrain_objects = [terrain_eval]
selected_objects = list(bpy.context.selected_objects)
# =========================
# MAIN LOOP
# =========================
for obj in selected_objects:
if "dybde" not in obj:
continue
depth = round(float(obj["dybde"]), 1)
label = f"{depth} m"
# ---- TEXT ----
curve = bpy.data.curves.new(type="FONT", name=f"DepthCurve_{obj.name}_{MODE}")
curve.body = label
curve.size = TEXT_SIZE
text_obj = bpy.data.objects.new(f"DepthLabel_{obj.name}_{MODE}", curve)
bpy.context.collection.objects.link(text_obj)
# ---- RAYCAST ----
origin_world = obj.location + mathutils.Vector((0, 0, 1000))
direction_world = mathutils.Vector((0, 0, -1))
hit = False
closest_loc_world = None
closest_normal_world = None
min_distance = float('inf')
for terrain_eval in terrain_objects:
mw = terrain_eval.matrix_world
mw_inv = mw.inverted()
origin_local = mw_inv @ origin_world
direction_local = (mw_inv.to_3x3() @ direction_world).normalized()
result, loc_local, normal_local, _ = terrain_eval.ray_cast(origin_local, direction_local)
if not result:
continue
loc_world = mw @ loc_local
normal_world = (mw.to_3x3() @ normal_local).normalized()
if normal_world.z < UPWARD_NORMAL_MIN_Z:
continue
dist = (origin_world - loc_world).length
if dist < min_distance:
min_distance = dist
closest_loc_world = loc_world
closest_normal_world = normal_world
hit = True
# ---- POSITION ----
if hit and closest_normal_world:
push_dir = closest_normal_world.normalized()
text_location = closest_loc_world + push_dir * (HEIGHT_OFFSET + SURFACE_OFFSET)
else:
text_location = obj.location + mathutils.Vector((0,0,HEIGHT_OFFSET))
text_obj.location = text_location
text_obj.data.materials.append(text_mat)
# Face up
text_obj.rotation_euler[0] = 1.5708
# BACK flip
if MODE == "BACK":
text_obj.rotation_euler[2] += 3.14159
# =========================
# DOT (ONLY FRONT + GEO)
# =========================
if MODE == "FRONT":
if hit and closest_normal_world:
dot_location = closest_loc_world + closest_normal_world * (DOT_SIZE + SURFACE_OFFSET)
else:
dot_location = obj.location + mathutils.Vector((0,0,DOT_SIZE))
bpy.ops.mesh.primitive_uv_sphere_add(radius=DOT_SIZE, location=dot_location)
dot = bpy.context.object
dot.data.materials.append(dot_mat)
print(f"Done ({MODE})")
Resultat
Nu kan modellen eksporteres som GLB eller GLTF, og dybdetekst følger automatisk med.

