Skip to content

Instantly share code, notes, and snippets.

@lemanschik
Created September 2, 2025 06:21
Show Gist options
  • Select an option

  • Save lemanschik/086ff75f4f8a18cc522cdbaabf6d769d to your computer and use it in GitHub Desktop.

Select an option

Save lemanschik/086ff75f4f8a18cc522cdbaabf6d769d to your computer and use it in GitHub Desktop.
vallheim.md

Excellent question! You've run into a classic problem in game map visualization, and your intuition is spot on. The "rounded map" is the absolute key to understanding the offset.

Let's break down exactly what's happening and how to fix it.

The Core Problem: Mismatched Coordinate Systems

You are dealing with two different coordinate systems:

  1. In-Game World Coordinates: These are the raw (X, Y, Z) coordinates your log parser is correctly reading. For a top-down map, we only care about (X, Z). This system is a perfect, flat Cartesian grid where the center of the world is (0, 0).
  2. Map Image Pixel Coordinates: This is the coordinate system of the PNG or JPEG file of your map. The origin (0, 0) is at the top-left corner. X increases to the right, and Y increases downwards.

The issue is that the map image is not a simple, direct screenshot of the in-game world. It's a projection. The game takes the circular world data and projects it onto a square image, which introduces three changes you need to account for:

  • Translation (Offset): The world origin (0, 0) is not at the image's top-left corner (0, 0). It's in the center of the image.
  • Scaling: One unit of distance in the game world (e.g., 1 meter) corresponds to a certain number of pixels on the map image.
  • Projection Distortion (The "Rounded" Effect): Especially near the edges, the map is slightly distorted to give it that circular, globe-like feel. For most of the central map area, this effect is minimal and we can often ignore it for a "good enough" result.

How to Fix It: A Step-by-Step Calibration Guide

To perfectly overlay your path, you need to find the mathematical formula that converts a world coordinate (world_x, world_z) to a pixel coordinate (pixel_x, pixel_y).

Here’s the simplest and most effective way to do this.

Step 1: Get Calibration Points

You need at least two, but preferably three, well-known points for which you know both the in-game world coordinates and the map image pixel coordinates.

  • Point 1 (The Easiest): The Sacrificial Stones at the world center.

    • World Coords: (0, 0) (or very close to it, like (5, -10)). Go there in-game and use the pos console command (F5 -> devcommands -> pos) to get the exact (X, Z).
    • Pixel Coords: Open your map image in any image editor (like Photoshop, GIMP, Paint.NET, or even MS Paint). Hover your mouse over the exact center of the Sacrificial Stones icon. The editor will show you the pixel coordinates (e.g., 1024, 1024 for a 2048x2048 map).
  • Point 2 (A Good Second): The Trader, Haldor.

    • World Coords: Find him in-game and use the pos command.
    • Pixel Coords: Find the bag icon on your map image and get his pixel coordinates.
  • Point 3 (For Accuracy): A Boss Altar or a prominent, distant island you've visited.

    • World Coords: Go there, use pos.
    • Pixel Coords: Find the corresponding icon/feature on the map image.

Let's say you get the following data:

  • Center: World (0, 0) -> Pixel (2048, 2048)
  • Trader: World (1500, 2500) -> Pixel (2824, 848)

(Note: I made up the Trader numbers for this example)

Step 2: Calculate the Transformation

The basic formula is:

pixel_x = (world_x * scale) + offset_x
pixel_y = (world_z * scale) + offset_y

1. Find the Offset: This is the easiest part. The world origin (0, 0) maps directly to the pixel center of your map. If your map image is 4096x4096 pixels, the center is at (2048, 2048).

  • offset_x = image_width / 2
  • offset_y = image_height / 2

So, for our example, offset_x = 2048 and offset_y = 2048.

2. Find the Scale: Now we can use our second point (the Trader) to find the scale. We need to solve for scale.

Let's check the X-axis first: Trader_pixel_x = (Trader_world_x * scale_x) + offset_x 2824 = (1500 * scale_x) + 2048 2824 - 2048 = 1500 * scale_x 776 = 1500 * scale_x scale_x = 776 / 1500 = 0.5173

Now let's check the Z-axis (which maps to the Y pixel axis): Trader_pixel_y = (Trader_world_z * scale_z) + offset_y 848 = (2500 * scale_z) + 2048 848 - 2048 = 2500 * scale_z -1200 = 2500 * scale_z scale_z = -1200 / 2500 = -0.48

Wait! Why are the scales different, and why is one negative?

  • Negative Scale: This is correct and expected! In world coordinates, a positive Z value means going North (up the map). But in pixel coordinates, a higher Y value means going down the image. Therefore, the Z-to-Y scale must be negative to flip the axis.
  • Different Scales: 0.5173 vs -0.48. They are close. This small difference is likely due to the projection distortion (the rounded map effect). For a "good enough" visualization, you can just average their absolute values.
    • average_scale = (abs(scale_x) + abs(scale_z)) / 2 = (0.5173 + 0.48) / 2 = 0.49865

Let's use this average scale. scale_x will be 0.49865 and scale_z will be -0.49865.

Step 3: Implement the Final Formula

Now you have everything you need. For any given (world_x, world_z) from your log file, you can calculate the pixel position.

Final Formula:

// Your calculated constants
map_image_width = 4096
map_image_height = 4096
scale = 0.49865 // The average scale you calculated

// The conversion function
pixel_x = (world_x * scale) + (map_image_width / 2)
pixel_y = (world_z * -scale) + (map_image_height / 2) // Note the negative scale for Z->Y

Python Code Example

Here is how you would implement this in a simple Python function:

# --- Constants you need to find for YOUR specific map image ---
MAP_IMAGE_WIDTH = 4096  # Width of your map.png in pixels
MAP_IMAGE_HEIGHT = 4096 # Height of your map.png in pixels

# This is the most important value you calculate from your calibration points.
# It represents how many pixels correspond to one in-game meter.
WORLD_TO_PIXEL_SCALE = 0.49865 # Using the example value we calculated

# --- The conversion function ---
def world_to_pixel(world_x, world_z):
    """
    Converts Valheim in-game world coordinates to pixel coordinates on the map image.
    """
    
    # Calculate the offset (the pixel coordinate of the world center)
    offset_x = MAP_IMAGE_WIDTH / 2
    offset_y = MAP_IMAGE_HEIGHT / 2
    
    # Apply scale and offset
    # Note: world Z is inverted for pixel Y (positive Z is North, but positive Y is Down)
    pixel_x = (world_x * WORLD_TO_PIXEL_SCALE) + offset_x
    pixel_y = (world_z * -WORLD_TO_PIXEL_SCALE) + offset_y
    
    return (int(pixel_x), int(pixel_y))

# --- Example Usage ---
# Coordinates from your log parser
player_world_pos = (1234.5, -678.9) # A sample (X, Z) coordinate

# Get the pixel position for drawing
px, py = world_to_pixel(player_world_pos[0], player_world_pos[1])

print(f"World position ({player_world_pos[0]}, {player_world_pos[1]}) maps to pixel ({px}, {py})")

# Example with our Trader calibration point to double-check
trader_world = (1500, 2500)
trader_px, trader_py = world_to_pixel(trader_world[0], trader_world[1])
print(f"Trader at {trader_world} maps to pixel ({trader_px}, {trader_py})") 
# This should print a result very close to the (2824, 848) you measured!

By applying this transformation to every coordinate in your walking history, your path should now overlay perfectly (or at least, very, very closely) on your map image. The slight "offset to the right" you were seeing was because you were likely missing either the scaling factor or the central offset translation.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment