Reputation: 103
I'm making a mode 7/perspective projection project in Godot. When I run it, it produces the expected effect, displaying a 2d image as if it were a 3d plane. Code:
func _ready():
map.load("res://map2.png")
perspective.load("res://map2.png")
for px in 1:
self.texture = display
for y in map.get_height():
_y = (y + py - 1)/z
for x in map.get_width():
_x = (x + px)/z
map.lock()
pix = map.get_pixel(_x, _y)
map.unlock()
perspective.lock()
perspective.set_pixel(x, y, pix)
perspective.unlock()
display.create_from_image(perspective)
z += 1
Image:
However, I have a problem. I have the code in the ready function, in a for loop. I want it to be called every frame, but when I increase the number of repeats from one to two, it turns the entire image red. I don't know what's causing this. one guess was that I wasn't locking and unlocking the images properly, but that is most likely not the case. Another guess was that the x and y variables were not resetting each time, but that was also working fine. I don't think the loop itself is the problem, but I have no idea what's wrong.
Upvotes: 1
Views: 307
Reputation: 40170
I struggled to make your code run. I half way gave up, and implemented the logic from my prior answer using lock bits instead. This is the code:
extends Sprite
export(Transform) var matrix:Transform
var sampler:Image
var buffer:Image
var size:Vector2
var center:Vector2
func _ready():
sampler = texture.get_data()
var err = sampler.decompress()
if err != OK:
push_error("Failed to decompress texture")
return
size = Vector2(texture.get_width(), texture.get_height())
center = size * 0.5
buffer = Image.new()
buffer.create(int(size.x), int(size.y), false, Image.FORMAT_RGBA8)
func _process(_delta):
#matrix = matrix.rotated(Vector3.RIGHT, 0.01)
sampler.lock()
buffer.lock()
for y in size.x:
for x in size.y:
var uv:Vector3 = matrix * Vector3(x - center.x, y - center.y, 1.0)
if uv.z <= 0.0:
buffer.set_pixel(x, y, Color.transparent)
continue
var _x = (uv.x / uv.z) + center.x
var _y = (uv.y / uv.z) + center.y
if _x < 0.0 or _x >= size.x or _y < 0.0 or _y >= size.y:
buffer.set_pixel(x, y, Color.transparent)
continue
#buffer.set_pixel(x, y, Color(_x / size.x, y / size.y, 0.0))
buffer.set_pixel(x, y, sampler.get_pixel(_x, _y))
buffer.unlock()
sampler.unlock()
var display = ImageTexture.new()
display.create_from_image(buffer, 0)
self.texture = display
As you can see, I'm exporting a Transfrom
to be available on the editor. That is a proper 3D Transform. There is a commented line on _process
that does a rotation, try it out.
The sampler
Image
is a copy of the Texture
of the Sprite
(the copy is made on _ready
). And the buffer
Image
is where what is to be displayed is constructed.
The code is creating an ImageTexture
from buffer
and replacing the current texture
with it, each frame (on _process
). I'm setting flags to 0
, because FLAG_REPEAT
plus FLAG_FILTER
blurred the border to the opposite side of the Sprite
.
The vector Vector2
size
holds the size of the texture. And the Vector2
Center
is the coordinates of the center.
As I said at the start, this is the logic from my prior answer. This line:
vec3 uv = matrix * vec3(UV - 0.5, 1.0);
Is equivalent to (except I'm not scaling the coordinates to the range from 0 to 1):
var uv:Vector3 = matrix * Vector3(x - center.x, y - center.y, 1.0)
Then I had this line:
if (uv.z < 0.0) discard;
Which turned out like this:
if uv.z <= 0.0:
buffer.set_pixel(x, y, Color.transparent)
continue
I'm setting transparent because I do not recreate the buffer, nor clear it before hand.
Finally this line:
COLOR = texture(TEXTURE, (uv.xy / uv.z) + 0.5);
Turned out like this:
var _x = (uv.x / uv.z) + center.x
var _y = (uv.y / uv.z) + center.y
if _x < 0.0 or _x >= size.x or _y < 0.0 or _y >= size.y:
buffer.set_pixel(x, y, Color.transparent)
continue
buffer.set_pixel(x, y, sampler.get_pixel(_x, _y))
As per the result, here is the Godot Icon "rotating in 3D" (not really, but that is the idea):
Please disregard visual artifact due to GIF encoding.
I'm not sure if you want to stay with the logic of my prior answer. However, I believe this one should not be too hard to modify for your needs.
I used a Transform because there is no convenient Matrix type available. However, the Transform uses a Matrix internally. See also Transformation matrix.
The Mode 7 formula according to Wikipedia works with a 2 by 2 Matrix, which is simpler that what I have here. However, you are going to need the product of a Matrix and a Vector anyway. You cannot compute the components independently.
This is the formula according to Wikipedia:
r' = M*(r - r_0) + r_0
That is:
var rp = mult(M, r - r_0) + r_0
Where mult
would look like this:
func mult(matrix, vector:Vector2) -> Vector2:
var x = vector.x * matrix.a + vector.y * matrix.b
var y = vector.x * matrix.c + vector.y * matrix.d
return Vector2(x, y)
However, as I said, there is no convenient matrix type. If we export a
, b
, c
, and d
, we have:
var rp = mult(a, b, c, d, r - r_0) + r_0
And mult
looks like this:
func mult(a:float, b:float, c:float, d:float, vector:Vector2) -> Vector2:
var x = vector.x * a + vector.y * b
var y = vector.x * c + vector.y * d
return Vector2(x, y)
We can easily use modify the code to do that. First export a
, b
, c
and d
as I said:
export(float) var a:float
export(float) var b:float
export(float) var c:float
export(float) var d:float
And this is _process
modified:
func _process(_delta):
sampler.lock()
buffer.lock()
for y in size.x:
for x in size.y:
var rp = mult(a, b, c, d, Vector2(x, y) - center) + center
if rp.x < 0.0 or rp.x >= size.x or rp.y < 0.0 or rp.y >= size.y:
buffer.set_pixel(x, y, Color.transparent)
continue
buffer.set_pixel(x, y, sampler.get_pixel(rp.x, rp.y))
buffer.unlock()
sampler.unlock()
var display = ImageTexture.new()
display.create_from_image(buffer, 6)
self.texture = display
Of course, mult
is the one I showed above. I'm assuming here that r_0
is what I called center
.
I'm not sure how to interpret a
, b
, c
and d
, so here is a = 1
, b = 2
, c = 3
and d = 4
:
Upvotes: 1