Skip to content

Pyray : convert function arguments based on c function signature #53

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions examples/models/models_animation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@

import pyray as ray


screen_width = 800
screen_height = 450

ray.init_window(screen_width, screen_height, "raylib [models] example - model animation")

# Define the camera to look into our 3d world
camera = ray.Camera3D()
camera.position = ray.Vector3( 10.0, 10.0, 10.0 ) # Camera position
camera.target = ray.Vector3( 0.0, 0.0, 0.0 ) # Camera looking at point
camera.up = ray.Vector3( 0.0, 1.0, 0.0 ) # Camera up vector (rotation towards target)
camera.fovy = 45.0 # Camera field-of-view Y
camera.projection = ray.CAMERA_PERSPECTIVE # Camera mode type

model = ray.load_model("resources/models/iqm/guy.iqm") # Load the animated model mesh and basic data
texture = ray.load_texture("resources/models/iqm/guytex.png") # Load model texture and set material
ray.set_material_texture(model.materials, ray.MATERIAL_MAP_ALBEDO, texture) # Set model material map texture

position = ( 0., 0., 0. ) # Set model position

# Load animation data
anims = ray.load_model_animations("resources/models/iqm/guyanim.iqm")
anim_frame_counter = 0

ray.set_camera_mode(camera, ray.CAMERA_FREE) # Set free camera mode

ray.set_target_fps(60) # Set our game to run at 60 frames-per-second
#--------------------------------------------------------------------------------------

# Main game loop
while not ray.window_should_close(): # Detect window close button or ESC key
# Update
#----------------------------------------------------------------------------------
ray.update_camera(camera)

# Play animation when spacebar is held down
if ray.is_key_down(ray.KEY_SPACE):
anim_frame_counter+=1
ray.update_model_animation(model, anims[0], anim_frame_counter)
if anim_frame_counter >= anims[0].frameCount:
anim_frame_counter = 0

#----------------------------------------------------------------------------------

# Draw
#----------------------------------------------------------------------------------
ray.begin_drawing()

ray.clear_background(ray.RAYWHITE)

ray.begin_mode_3d(camera)

ray.draw_model_ex(model, position, ray.Vector3( 1.0, 0.0, 0.0 ), -90.0, ray.Vector3( 1.0, 1.0, 1.0 ), ray.WHITE)

for i in range(model.boneCount):
ray.draw_cube(anims[0].framePoses[anim_frame_counter][i].translation, 0.2, 0.2, 0.2, ray.RED)

ray.draw_grid(10, 1.0) # Draw a grid

ray.end_mode_3d()

ray.draw_text("PRESS SPACE to PLAY MODEL ANIMATION", 10, 10, 20, ray.MAROON)
ray.draw_text("(c) Guy IQM 3D model by @culacant", screen_width - 200, screen_height - 20, 10, ray.GRAY)

ray.draw_fps(10, 400)

ray.end_drawing()
#----------------------------------------------------------------------------------


# De-Initialization
#--------------------------------------------------------------------------------------
ray.unload_texture(texture) # Unload texture

# Unload model animations data
for anim in anims:
ray.unload_model_animation(anim)

ray.unload_model(model) # Unload model

ray.close_window() # Close window and OpenGL context
Binary file added examples/models/resources/models/iqm/guy.blend
Binary file not shown.
Binary file added examples/models/resources/models/iqm/guy.iqm
Binary file not shown.
Binary file added examples/models/resources/models/iqm/guyanim.iqm
Binary file not shown.
Binary file added examples/models/resources/models/iqm/guytex.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
8 changes: 4 additions & 4 deletions examples/textures/textures_bunnymark.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ def __init__(self):
while not WindowShouldClose(): #// Detect window close button or ESC key
#// Update
#//----------------------------------------------------------------------------------
if IsMouseButtonDown(MOUSE_LEFT_BUTTON):
if IsMouseButtonDown(MOUSE_BUTTON_LEFT):
#// Create more bunnies
for i in range(0, 100):
if bunniesCount < MAX_BUNNIES:
Expand Down Expand Up @@ -90,11 +90,11 @@ def __init__(self):

DrawRectangle(0, 0, screenWidth, 40, BLACK)
text = f"bunnies {bunniesCount}"
DrawText(text.encode('utf-8'), 120, 10, 20, GREEN)
DrawText(text.encode('utf-8'), 120, 20, 20, GREEN)
text = f"batched draw calls: { 1 + int(bunniesCount/MAX_BATCH_ELEMENTS)}"
DrawText(text.encode('utf-8'), 320, 10, 20, MAROON)
DrawText(text.encode('utf-8'), 320, 20, 20, MAROON)

DrawFPS(10, 10)
DrawFPS(20, 20)

EndDrawing()
#//----------------------------------------------------------------------------------
Expand Down
2 changes: 1 addition & 1 deletion examples/textures/textures_bunnymark_dynamic.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ def __init__(self):
while not rl.WindowShouldClose(): #// Detect window close button or ESC key
#// Update
#//----------------------------------------------------------------------------------
if rl.IsMouseButtonDown(rl.MOUSE_LEFT_BUTTON):
if rl.IsMouseButtonDown(rl.MOUSE_BUTTON_LEFT):
#// Create more bunnies
for i in range(0, 100):
if bunniesCount < MAX_BUNNIES:
Expand Down
8 changes: 4 additions & 4 deletions examples/textures/textures_bunnymark_more_pythonic.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ def __init__(self):
while not WindowShouldClose(): #// Detect window close button or ESC key
#// Update
#//----------------------------------------------------------------------------------
if IsMouseButtonDown(MOUSE_LEFT_BUTTON):
if IsMouseButtonDown(MOUSE_BUTTON_LEFT):
#// Create more bunnies
for i in range(0, 100):
if bunniesCount < MAX_BUNNIES:
Expand Down Expand Up @@ -89,11 +89,11 @@ def __init__(self):

DrawRectangle(0, 0, screenWidth, 40, BLACK)
text = f"bunnies {bunniesCount}"
DrawText(text.encode('utf-8'), 120, 10, 20, GREEN)
DrawText(text.encode('utf-8'), 120, 20, 20, GREEN)
text = f"batched draw calls: { 1 + int(bunniesCount/MAX_BATCH_ELEMENTS)}"
DrawText(text.encode('utf-8'), 320, 10, 20, MAROON)
DrawText(text.encode('utf-8'), 320, 20, 20, MAROON)

DrawFPS(10, 10)
DrawFPS(10, 20)

EndDrawing()
#//----------------------------------------------------------------------------------
Expand Down
101 changes: 75 additions & 26 deletions pyray/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,12 @@
# SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0

from raylib import rl, ffi

from inspect import ismethod,getmembers,isbuiltin
import inflection

current_module = __import__(__name__)



def pointer(self, struct):
return ffi.addressof(struct)

LIGHTGRAY =( 200, 200, 200, 255 )
GRAY =( 130, 130, 130, 255 )
DARKGRAY =( 80, 80, 80, 255 )
Expand Down Expand Up @@ -60,31 +55,74 @@ def pointer(self, struct):
# Another possibility is ffi.typeof() but that will throw an exception if you give it a type that isn't a ctype
# Another way to improve performance might be to special-case simple types before doing the string comparisons

def makefunc(a):
#print("makefunc ",a, ffi.typeof(a).args)

# C_POINTER : used to determine whether to use ffi.addressof
# NOTE : I put C_POINTER assignment in a function so temp variable gets deallocated after use
# + if there's a cleaner way to get the type _cffi_backend.__CDataOwn, feel free to correct this
def initPointerDefinition():
temp = ffi.new('struct Vector3 *', (0,0,0))[0]
global C_POINTER
C_POINTER = type(temp)
#print(C_POINTER)

## simple value converters
# will fail if fed wrong arguments

def to_bytes(value):
if type(value) is bytes: return value
else: return value.encode('utf-8', 'ignore')

def to_str(value):
if value == ffi.NULL: return ''
return ffi.string(value).decode('utf-8')


initPointerDefinition()
def to_pointer(value):
#print(value)
if type(value) is C_POINTER:
return ffi.addressof(value)
return value


def makeFunc(c_func):
#print("makefunc ", c_func, ffi.typeof(c_func).args)

# based on ctypes of arguments of the c function
# we build a list of converters to call on python function arguments
argConverters = []
for c_arg_type in ffi.typeof(c_func).args:
if c_arg_type is ffi.typeof('char *'):
argConverters.append(to_bytes)
elif c_arg_type.kind == 'pointer':
argConverters.append(to_pointer)
else:
argConverters.append(None) # None = leave as is

# not sure if this would bring any speedup
#argConverters = tuple(argConverters)

# convert the function's returned value
resultConverter = None # None = leave as is
c_result_type = ffi.typeof(c_func).result
if c_result_type is ffi.typeof('char *'):
resultConverter = to_str

# use a closure to bring converters into c function call
def func(*args):
modified_args = []
for (c_arg, arg) in zip(ffi.typeof(a).args, args):
#print(arg, c_arg.kind)
if type(arg) == str:
encoded = arg.encode('utf-8')
modified_args.append(encoded)
elif c_arg.kind == 'pointer' and str(type(arg)) == "<class '_cffi_backend.__CDataOwn'>":
modified_args.append(ffi.addressof(arg))
else:
modified_args.append(arg)
result = a(*modified_args)
nonlocal argConverters, resultConverter

result = c_func(* (convert(arg) if convert else arg for (arg, convert) in zip(args, argConverters) ) )

if result is None:
return
if str(type(result)) == "<class '_cffi_backend._CDataBase'>" and str(result).startswith("<cdata 'char *'"):
if str(result) == "<cdata 'char *' NULL>":
result = ""
else:
result = ffi.string(result).decode('utf-8')
if resultConverter:
return resultConverter(result)
return result

return func

def makeStructHelper(struct):
def makeStruct(struct):
def func(*args):
return ffi.new(f"struct {struct} *", args)[0]
return func
Expand All @@ -99,7 +137,7 @@ def func(*args):
#print(attr.__text_signature__)
#print(dir(attr))
#print(dir(attr.__repr__))
f = makefunc(attr)
f = makeFunc(attr)
setattr(current_module, uname, f)
#def wrap(*args):
# print("call to ",attr)
Expand All @@ -108,6 +146,17 @@ def func(*args):
setattr(current_module, name, attr)

for struct in ffi.list_types()[0]:
f = makeStructHelper(struct)
f = makeStruct(struct)
setattr(current_module, struct, f)

#################
## manual wrapping for edge cases

# passing argument to be initialized in function is not supported in Python
def load_model_animations(animpath):
count = ffi.new("unsigned int *", 0)
result = rl.LoadModelAnimations(to_bytes(animpath), count)
count = count[0]
#print(count)
return [result[i] for i in range(count)]