Defold Rendy

Hi all,

Defold Rendy provides a versatile camera suite and render pipeline in a Defold game engine project.

I logged some of Rendy’s development throughout the past couple weeks in the following thread, so please check it out if you’re interested: New Camera Extension - Progress Updates (and Suggestions?)

While the goal of Rendy was to prove to myself that I could write an entire camera system and render pipeline from scratch, it turned into a project that I hope can provide as many out-of-the-box rendering features for both 2D and 3D games.

Testing every function in several different scenarios was very time consuming, so I am releasing Rendy a bit earlier than expected to potentially get some feedback from the Defold community. No doubt there will be unforeseen issues to fix. I really appreciate the help I’ve received from other Defold members over the years.

Happy Defolding!

27 Likes

Amazing! Well done! Very needed.

3 Likes

This is great! Thank you Klayton!

Do you have any Patreon or Ko-Fi page?

2 Likes

No donation page, but thanks for the consideration.

2 Likes

@WhiteBoxDev I I have a quick question; Is ‘rendy.screen_to_world’ expects vector3?

When I pass the vec3 position it fails, am I missing something?:

local player_pos = go.get_position('/cube')
local player_screen_pos = rendy.screen_to_world(id_rendy, player_pos) 
-- << rendy/rendy.lua:265: argument #-1 contains one or more values which are not numbers: vmath.vector4(nan, nan, nan, nan)

Thank you

In this you are passing a world coordinate into screen_to_world() which probably fails because it is not in the correct coordinate space and will fail the call to is_within_viewport().

I have a related question here.

1 Like

You are right, it doesn’t make sense, but it shouldn’t be a matter since we can pass any vec3 as screen coordinate.

Error is on line 265, so it pass the is_within_viewport()

local player_pos = go.get_position('/cube')
pprint(player_pos) -- << vmath.vector3(-2, 0, 0)
local player_screen_pos = rendy.world_to_screen(id_rendy, player_pos)
pprint(player_screen_pos) -- << nil

Also world_to_screen returns nil if the gameobject is not at 0,0,0. I guess I’m doing something wrong here!

I’ve started using Rendy for a new project and it’s great! So simple to set up. Thanks for this!

My first question. Is 3D model transparency an option? I saw this comment in the render script:

– Blending is not supported for 3D objects because Defold does not sort them from back to front, which is a requirement for proper blending.

Does this mean the textures of 3D objects can’t be transparent?

3 Likes

Are you looking to achieve partial transparency? If so, then that’s usually done by sorting 3D meshes from back to front, or farther to closer, relative to the camera.

I remember reading that Defold doesn’t auto-sort 3D objects within a batch like it does with 2D objects. For example, when drawing 100 sprite components in a single batch, Defold sorts them based on their distance from the camera, which is set by each component’s z value. The result of this sorting operation is that sprites that are closer to the camera are drawn last, and therefore show up “in front of” the sprites behind them.

Untitled

In the above image, each box is a sprite with the corresponding z value. The greater the z value, the closer to the camera, and the last to be drawn to the screen. If Defold decided not to sort them, then you can imagine these sprites appearing above or below other sprites in a seemingly random way.

If objects are not automatically sorted in a batch, then a depth buffer can be used in the render script as an alternative method to achieve that sorted look. As far as I know, Defold doesn’t sort 3D objects, so this depth buffer solution must be used instead.

So, what does this have to do with partial transparency? Let’s take a look. The following image shows 3D objects that are drawn to the screen in a random order, but use a depth buffer to make sure the correct object appears in front.

Untitled2

Looks okay. The problem arises when we try to make the green cube, say, 50% transparent. We would expect to see the red cube behind it through some basic alpha blending, right? But that’s not what happens. Remember we’re using a depth buffer, so the red cube’s fragments that are behind the green cube are discarded. The result is that part of the red cube simply isn’t drawn, effectively canceling the green cube’s transparency.

If we attempt to hack around this by removing the depth buffer, then there’s no guarantee which object will be drawn on top of the other, which of course is unacceptable. Imagine standing on a road looking at the front of a house, but instead of seeing the walls and front door, you see some of the kitchen appliances, a person lying in a bedroom, and a tree in the backyard floating in front of the house!

(The reason for back-to-front sorting instead of front-to-back is because alpha blending samples whatever fragment color already exists at that location. If transparent objects are drawn before all of the opaque objects are finished drawing, then whichever opaque objects haven’t been drawn yet won’t blend with the transparent ones. This would be like looking through a window and half of the objects outside disappearing, while the other half appear as expected.)

Like I’ve mentioned, the usual solution is to sort 3D objects by their distance from the camera, similar to how it’s done with sprites. There are however some complications and corner cases that are likely game-dependent on how to deal with them, like when some of a rotating object’s vertices swap z closeness with another object. This might be one of the issues that make it less obvious how to implement this feature in a stable and satisfying way.

Another solution you might use is instead of using the depth buffer and discarding farther fragments, you could switch blend functions to additive or multiplicative instead of the usual alpha mode. This would lighten or darken overlapping objects instead of completely remove them, but that coloring effect is likely undesirable most of the time.

There are supposedly other solutions out there that don’t require back-to-front sorting, however I’ve never implemented them myself, so I’m not sure how difficult or computationally expensive they are. Feel free to research them if that’s something you find interesting. I’d really like to hear about what you find, and if it’s doable with what Defold offers at the moment.

Edit: Ah, I forgot to mention. This is all assuming these 3D objects are drawn within the same batch. If you dedicate draw calls specifically to partially transparent objects, and draw them after the opaque objects, then partial transparency will work, assuming a depth buffer and alpha blending are enabled. This requires a number of additional predicates, materials, etc.

7 Likes

Great writeup.

It looks like the most straightforward way of achieving this is to add an additional draw call for transparent objects. In my case quads acting as a shadow underneath models (or sprites actually, that might be slightly cheaper).

Thanks!

2 Likes

Is there a way Rendy could support something similar to screen_to_world_ray() from Rendercam?

Being able to add Rendy to a moving game object is amazing, which is why I’d like to use it instead of Rendercam.

I haven’t found a way to detect taps in the 3D space, so that’s what I’m trying to solve. Being able to get the coordinates to be used in a raycast would be wonderful!

1 Like

Unless I’m missing some minor detail, it looks like rendercam’s screen_to_world_ray() function is doing the same thing as Rendy’s screen_to_world() function, but once for the near plane’s position and once for the far plane’s position:

local position_start, position_end = vmath.vector3(x, y, near), vmath.vector3(x, y, far)
local ray_start, ray_end = rendy.screen_to_world(rendy_id, position_start), rendy.screen_to_world(rendy_id, position_end)

Can you test that in your project and let me know if the results are incorrect?

1 Like

Will do, thanks!

Edit: Yes, this works! This code works in borth ortho and perspective modes:

local rendy_id = go.get_id("/rendy")
local RAY_LENGTH = 10000
local position_start = vmath.vector3(action.screen_x, action.screen_y, RAY_LENGTH/2)
local position_end = vmath.vector3(action.screen_x, action.screen_y, -RAY_LENGTH/2)
local ray_start = rendy.screen_to_world(rendy_id, position_start)
local ray_end = rendy.screen_to_world(rendy_id, position_end)

if ray_start then
	msg.post("@render:", "draw_line", { start_point = ray_start, end_point = ray_end, color = vmath.vector4(1, 0.5, 0, 1) } )
	local hit = physics.raycast(ray_start, ray_end, {hash("character")})
	print(hit)
end

On another note. I did some detective work in regards to z_min and z_max, and found the following:

  • In ortho a negative z_min and positive z_max works.
  • In perspective a negative z_min and positive z_max appear inside out.
1 Like

I also have issues with rendy.screen_to_world().

This:

function on_input(self, action_id, action)
	if action_id == hash("touch") then
		if action.pressed then

			local world_position = vmath.vector3(action.x, action.y, 0)
			print("world_position", world_position)
			local camera_id = hash("/rendy")
			local position = rendy.world_to_screen(camera_id, world_position)
			print(position)

		end
	end
end

Results in this:

DEBUG:SCRIPT: world_position	vmath.vector3(221.74998474121, 470.25, 0)
DEBUG:SCRIPT: nil

Is there a way to convert touch world coordinates to screen coordinates in Rendy?

Update: I forgot about gui.set_screen_position(), so in this case I don’t need rendy.world_to_screen().

Rendy expects screen_x and screen_y, since those are responsive to changes in window size, rather than just referencing the initial display size set in the game.project file. If you’re getting nil back, then that means the position you sent to world_to_screen() lies outside the boundaries of the screen.

1 Like

Please make a release on Github! So it’s easier to link to specific versions. :slight_smile: