- Home /
How to draw a pixel on a raw image that corresponds to a 3D object's coordinates?
What I am trying to do is use the SetPixel function and draw pixels on a 2d image within a canvas that correspond to the position of a 3d object in the world. The 3d object is a model of a town attached to a terrain chunk. I can successfully get the image representing the terrain.
I currently have this code, and it works to some extent: the pixels are drawn on the image, but their position is incorrect.
Vector3 pos = obj.TransformPoint(obj.position);
image.SetPixel((int)pos.x, (int)pos.z, Color.red);
I assume it's not as easy as that, could anyone provide any help? I'd be grateful for any assistance, even some pseudocode would be great.
I have not done any thing like this but I'm guessing you will have to do some scaling down. Say your map scale is 100 times larger than the radar image you would have to scale the position down by 100 try dividing x and y by your scale.
Thanks for the reply, but this doesn't seem to work. It seems like more should be done, I suppose the position of the object needs to be mapped to the image within its bounds, but I'm stuck beyond calculating the bounds...
Answer by unity_ek98vnTRplGj8Q · Mar 03, 2021 at 11:59 PM
You need to know
a) The size of the terrain you are trying to map b) The resolution of the image that you are trying to map c) The origin location of the town in the world (either its upper left or lower left corner, I don't remember)
Then to calculate the corresponding pixel you need to get the position of the object relative to the terrain, normalize that position relative to the terrain size (in other words, on a scale of 0 to 1 how far to the right side of the terrain is my object), then scale back up by the resolution of your texture to find the specific pixel.
float terrainSizeX;
float terrainSizeZ;
Vector3 terrainCornerPosition;
int texResX;
int texResY;
Vector3 worldPos = obj.TransformPoint(obj.position);
Vector3 posRelativeToTerrain = worldPos - TerrainCornerPosition;
Vector3 normalizedPositionRelativeToTerrain = posRelativeToTerrain / new Vector3(terrainSizeX, 0, terrainSizeZ);
int pixelPosX = Mathf.RoundToInt(normalizedPositionRelativeToTerrain.x) * texResX;
int pixelPosY = Mathf.RoundToInt(normalizedPositionRelativeToTerrain.z) * texResY;
You also may or may not have to flip the sign of the pixelPosY value
Thank you for replying. I tried implementing your idea, but I'm not sure how to proceed. First, I'm not sure how to get the "terrainCornerPosition" and, secondly, normalizedPositionRelativeToTerrain tells me that the "/" operator cannot be applied to Vector3...
If you were to place an object in your world that corresponds to the upper left pixel on your texture, what would the position of that object be?
And oops I guess you have to divide out each component of the vector - = new Vector3(posRelativeToTerrain.x / terrainSizeX, 0, posRelativeToTerrain.z / terrainSizeZ);
Thank you for your help! I've made some progress, here's the code I have so far:
//I get the bounds of the terrain chunk
float terrainSizeX = terrainChunk.GetComponent().bounds.size.x;
float terrainSizeZ = terrainChunk.GetComponent().bounds.size.z;
//This step I am still unsure of. The upper left corner of a terrain chunk will vary //depending on which terrain chunk it is. For one chunk this might be Vector3(-61, 0, 61) and another one could be Vector3(61, 0, 61), etc... But for testing, I am using the center chunk's upper left corner position:
//Vector3 terrainCornerPosition = new Vector3(-61f, 0f, -61f);
//This is the resolution of the image (i.e. its width and height as seen in the inspector. Is that what you were referring to?)
int texResX = 500;
int texResY = 500;
// Now I iterate over all the towns on the terrain chunk and set the pixels on the image according to the town's position
foreach (Transform child in terrainChunk.transform) {
Vector3 pos = child.TransformPoint(child.position);
Vector3 posRelativeToTerrain = pos - terrainCornerPosition;
Vector3 normalizedPositionRelativeToTerrain = new
Vector3(posRelativeToTerrain.x / terrainSizeX, 0, posRelativeToTerrain.z / terrainSizeZ);
int pixelPosX = Mathf.RoundToInt(normalizedPositionRelativeToTerrain.x) * texResX;
int pixelPosY = Mathf.RoundToInt(normalizedPositionRelativeToTerrain.z) * texResY;
topoMap.SetPixel(pixelPosX, pixelPosY, Color.red);
}
However, now I get no pixels drawn for towns at all... I must be making a mistake somewhere with regards to the position of the town or the upper left corner. I even tried the other corners of the chunk, but no luck...
Answer by andrew-lukasik · Mar 04, 2021 at 12:32 AM
Unity3DPaint.cs
This code is an example of how to translate world point into a texture coordinate that's being projected as a rectangular shape of arbitrary rotation. It proves it by allowing you to draw with mouse in game view - but the general idea will stay the same whatever you want to do here exactly (to replace Material
with RawImage
I guess).
using UnityEngine;
using UnityEngine.InputSystem;
public class Unity3DPaint : MonoBehaviour
{
[SerializeField][Min(4)] int _textureWidth = 64;
[SerializeField][Min(4)] int _textureHeight = 64;
[SerializeField] Vector2 _regionWorldSize = new Vector2{ x=100 , y=100 };
Texture2D _texture;
[SerializeField] Material _dstMaterial = null;
#if UNITY_EDITOR
void OnValidate () { if( _texture!=null && ( _texture.width!=_textureWidth || _texture.height!=_textureHeight ) ) { Dispose(); CreateNewTexture(); } }
void OnDrawGizmos ()
{
GetAreaWorldCorners( region:_regionWorldSize , forTransform:transform , TR:out var tr , TL:out var tl , BR:out var br , BL:out var bl );
Gizmos.color = Color.red; Gizmos.DrawLine(tl,tr);
Gizmos.color = new Color(0,1,1,1); Gizmos.DrawLine(bl,br);
Gizmos.color = Color.green; Gizmos.DrawLine(bl,tl);
Gizmos.color = new Color(1,0,1,1); Gizmos.DrawLine(br,tr);
}
#endif
void OnEnable () => CreateNewTexture();
void OnDisable () => Dispose();
void Update ()
{
var mouse = Mouse.current;
if( mouse!=null && mouse.leftButton.isPressed )
{
Vector3 origin = transform.position;
Vector3 normal = transform.forward;
var camera = Camera.main;
var plane = new Plane( inNormal:normal , inPoint:origin );
var ray = camera.ScreenPointToRay(Input.mousePosition);
if( plane.Raycast(ray,out float hitDist) )
{
Vector3 point = ray.origin + ray.direction * hitDist;
bool isPointInsideRegion = PointRegionProjection( out Vector2 coord , point:point , region:_regionWorldSize , center:origin , horizontal:-transform.right , vertical:transform.up );
if( isPointInsideRegion )
{
Debug.DrawLine( camera.transform.position , point , Color.white , 0.1f );
_texture.SetPixel( (int)( _texture.width * coord.x ) , (int)( _texture.height * coord.y ) , Color.red );
_texture.Apply();
}
else Debug.DrawLine( camera.transform.position , point , Color.black , 0.1f );
}
}
}
void CreateNewTexture ()
{
_texture = new Texture2D( width:_textureWidth , height:_textureHeight , textureFormat:TextureFormat.ARGB32 , mipCount:3 , linear:true );
_texture.filterMode = FilterMode.Point;// no smooth pixels
_texture.SetPixel( 0 , 0 , Color.black );
_texture.SetPixel( x:_texture.width-1 , y:_texture.height-1 , Color.magenta );
_texture.Apply();
if( _dstMaterial!=null ) _dstMaterial.mainTexture = _texture;
}
void Dispose () => Dispose( _texture );
void Dispose ( Object obj )
{
#if UNITY_EDITOR
if( Application.isPlaying ) { UnityEngine.Object.Destroy( obj ); }
else { UnityEngine.Object.DestroyImmediate( obj ); }
#else
UnityEngine.Object.Destroy( thisObject );
#endif
}
/// <summary>Projects world point onto a region of a plane with arbitrary rotation.</summary>
/// <returns>True when coordinate is inside given region.</returns>
/// <param name="coordinates">UV-type coordinates.</param>
/// <param name="point">World point you want to project.</param>
/// <param name="region">Region absolute world space size.</param>
/// <param name="center">Region center (world space).</param>
/// <param name="horizontal">Horizontal axis (world space).</param>
/// <param name="vertical">Vertical axis (world space).</param>
public static bool PointRegionProjection ( out Vector2 coordinates , Vector3 point , Vector2 region , Vector3 center , Vector3 horizontal , Vector3 vertical )
{
GetAreaWorldCorners( region:region , center:center , horizontal:horizontal , vertical:vertical , TR:out var TR , TL:out var TL , BR:out var BR , BL:out var BL );
Vector3 pointRelativeToBLCorner = point - BL;
Vector3 H = Vector3.Project( vector:pointRelativeToBLCorner , onNormal:horizontal );
Vector3 V = Vector3.Project( vector:pointRelativeToBLCorner , onNormal:vertical );
Vector2 regionalCoords = new Vector2{
x = H.magnitude * Mathf.Sign( Vector3.Dot(H,horizontal) ) ,
y = V.magnitude * Mathf.Sign( Vector3.Dot(V,vertical) )
};
coordinates = regionalCoords / region;
return( coordinates.x>0 && coordinates.y>0 && coordinates.x<1f && coordinates.y<1f );
}
/// <param name="region">Region absolute world space size.</param>
/// <param name="center">Region center (world space).</param>
/// <param name="horizontal">Horizontal axis (world space).</param>
/// <param name="vertical">Vertical axis (world space).</param>
/// <param name="TR">Top-right corner (world space).</param>
/// <param name="TL">Top-left corner (world space).</param>
/// <param name="BR">Bottom-right corner (world space).</param>
/// <param name="BL">Bottom-left corner (world space).</param>
public static void GetAreaWorldCorners (
Vector2 region ,
Vector3 center , Vector3 horizontal , Vector3 vertical ,
out Vector3 TR , out Vector3 TL , out Vector3 BR , out Vector3 BL
)
{
horizontal = horizontal.normalized;
vertical = vertical.normalized;
Vector3 horizontalExtent = horizontal * region.x * 0.5f;
Vector3 verticalExtent = vertical * region.y * 0.5f;
TR = center + horizontalExtent + verticalExtent;
TL = center + -horizontalExtent + verticalExtent;
BR = center + horizontalExtent + -verticalExtent;
BL = center + -horizontalExtent + -verticalExtent;
}
/// <inheritdoc/>
/// <param name="forTransform">Transform to source <paramref name="center"/>, <paramref name="horizontal"/> and <paramref name="vertical"/> values from.</param>
public static void GetAreaWorldCorners (
Vector2 region ,
Transform forTransform ,
out Vector3 TR , out Vector3 TL , out Vector3 BR , out Vector3 BL
)
{
Vector3 center = forTransform.position;
Vector3 left = -forTransform.right;
Vector3 up = forTransform.up;
GetAreaWorldCorners( region:region , center:center , horizontal:left , vertical:up , TR:out TR , TL:out TL , BR:out BR , BL:out BL );
}
}
TransformTexturePosition.cs
till overcomplicated example but fits a bit more what you're trying to do here. (it calls methods defined in Unity3DPaint class)
using UnityEngine;
public class TransformTexturePosition : MonoBehaviour
{
[SerializeField] Vector3 _textureWorldHorizontalAxis = new Vector3( 1 , 0 , 0 );
[SerializeField] Vector3 _textureWorldVerticalAxis = new Vector3( 0 , 0 , 1 );
[SerializeField] Vector2 _textureWorldSize = new Vector2{ x=100 , y=100 };
[SerializeField] Vector3 _textureWorldCenter = Vector3.zero;
[SerializeField][Min(4)] int _textureWidth = 64;
[SerializeField][Min(4)] int _textureHeight = 64;
Texture2D _texture;
[SerializeField] Material _dstMaterial = null;
#if UNITY_EDITOR
void OnValidate () { if( _texture!=null && ( _texture.width!=_textureWidth || _texture.height!=_textureHeight ) ) { Dispose(); CreateNewTexture(); } }
void OnDrawGizmos ()
{
Unity3DPaint.GetAreaWorldCorners(
region: _textureWorldSize ,
center: _textureWorldCenter ,
horizontal: _textureWorldHorizontalAxis ,
vertical: _textureWorldVerticalAxis ,
TR:out var tr , TL:out var tl , BR:out var br , BL:out var bl
);
Gizmos.color = Color.red; Gizmos.DrawLine(tl,tr);
Gizmos.color = new Color(0,1,1,1); Gizmos.DrawLine(bl,br);
Gizmos.color = Color.green; Gizmos.DrawLine(bl,tl);
Gizmos.color = new Color(1,0,1,1); Gizmos.DrawLine(br,tr);
}
#endif
void OnEnable () => CreateNewTexture();
void OnDisable () => Dispose();
void Update ()
{
Vector3 objectWorldPosition = transform.position;
bool isObjectCoordInBounds = Unity3DPaint.PointRegionProjection(
coordinates: out Vector2 coord ,
point: objectWorldPosition ,
region: _textureWorldSize ,
center: _textureWorldCenter ,
horizontal: _textureWorldHorizontalAxis ,
vertical: _textureWorldVerticalAxis
);
if( isObjectCoordInBounds )
{
_texture.SetPixel( (int)(_texture.width*coord.x) , (int)(_texture.height*coord.y) , Color.red );
_texture.Apply();
}
}
void CreateNewTexture ()
{
_texture = new Texture2D( width:_textureWidth , height:_textureHeight , textureFormat:TextureFormat.ARGB32 , mipCount:3 , linear:true );
_texture.filterMode = FilterMode.Point;// no smooth pixels
_texture.SetPixel( 0 , 0 , Color.black );
_texture.SetPixel( x:_texture.width-1 , y:_texture.height-1 , Color.magenta );
_texture.Apply();
if( _dstMaterial!=null ) _dstMaterial.mainTexture = _texture;
}
void Dispose () => Dispose( _texture );
void Dispose ( Object obj )
{
#if UNITY_EDITOR
if( Application.isPlaying ) { UnityEngine.Object.Destroy( obj ); }
else { UnityEngine.Object.DestroyImmediate( obj ); }
#else
UnityEngine.Object.Destroy( thisObject );
#endif
}
}
Hey, thank you for sharing your code! I'm going through it and trying to apply parts of it to my case. Since I need to map the position of my 3d object (a town) to the texture, I assume what you meant by "translate world point into a texture coordinate" is basically represented by this part: Vector3 point = ray.origin + ray.direction * hitDist -> where, if I understand it correctly, you are shooting a ray from the camera to the plane, recording the point where it hits, and then remapping that point to the texture? Is this what you suggest I do, only by shooting a ray from the camera to the object I wish to record on the texture?
Exactlyif I understand it correctly, you are shooting a ray from the camera to the plane, recording the point where it hits, and then remapping that point to the texture?
Is this what you suggest I do, only by shooting a ray from the camera to the object I wish to record on the texture?
No need for raycasting when you know gameObject's position already. Do something like this:
Vector3 textureHorizontalAxis = new Vector3( 1 , 0 , 0 );
Vector3 textureVerticalAxis = new Vector3( 0 , 0 , 1 );
Vector3 textureWorldCenter = Vector3.zero;// << SET THIS VALUE
Vector2 textureWorldSize = Vector3.zero;// << SET THIS VALUE
Vector3 objectWorldPosition = Vector3.zero;// << SET THIS VALUE
bool isObjectCoordInsideTexture = PointRegionProjection(
coordinates: out Vector2 coord ,
point: objectWorldPosition ,
region: textureWorldSize ,
center: textureWorldCenter ,
horizontal: textureHorizontalAxis ,
vertical: textureVerticalAxis
);
if( isObjectCoordInsideTexture )
{
_texture.SetPixel( (int)(_texture.width*coord.x) , (int)(_texture.height*coord.y) , Color.red );
_texture.Apply();
}
Overall I think my answer is vastly overcomplicated here. @unity_ek98vnTRplGj8Q 's answer is much easier to understand and fits your requirements best.
Your answer
Follow this Question
Related Questions
How can i make terrains from 2d map images 0 Answers
Image Based Reflection 0 Answers
3D models from 2D images 1 Answer
Only showing parts of objects that are on a specified area 0 Answers
Hex Map on a Sphere 0 Answers