diff --git a/examples/models/models_animation.py b/examples/models/models_animation.py new file mode 100644 index 0000000..d481f89 --- /dev/null +++ b/examples/models/models_animation.py @@ -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 \ No newline at end of file diff --git a/examples/models/resources/models/iqm/guy.blend b/examples/models/resources/models/iqm/guy.blend new file mode 100644 index 0000000..3880467 Binary files /dev/null and b/examples/models/resources/models/iqm/guy.blend differ diff --git a/examples/models/resources/models/iqm/guy.iqm b/examples/models/resources/models/iqm/guy.iqm new file mode 100644 index 0000000..36bed5e Binary files /dev/null and b/examples/models/resources/models/iqm/guy.iqm differ diff --git a/examples/models/resources/models/iqm/guyanim.iqm b/examples/models/resources/models/iqm/guyanim.iqm new file mode 100644 index 0000000..824a68a Binary files /dev/null and b/examples/models/resources/models/iqm/guyanim.iqm differ diff --git a/examples/models/resources/models/iqm/guytex.png b/examples/models/resources/models/iqm/guytex.png new file mode 100644 index 0000000..05a58ee Binary files /dev/null and b/examples/models/resources/models/iqm/guytex.png differ diff --git a/examples/models/resources/models/iqm/vertex_colored_object.iqm b/examples/models/resources/models/iqm/vertex_colored_object.iqm new file mode 100644 index 0000000..ad0db07 Binary files /dev/null and b/examples/models/resources/models/iqm/vertex_colored_object.iqm differ diff --git a/examples/textures/textures_bunnymark.py b/examples/textures/textures_bunnymark.py index 7898a47..5044db1 100644 --- a/examples/textures/textures_bunnymark.py +++ b/examples/textures/textures_bunnymark.py @@ -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: @@ -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() #//---------------------------------------------------------------------------------- diff --git a/examples/textures/textures_bunnymark_dynamic.py b/examples/textures/textures_bunnymark_dynamic.py index 9c461f6..4c3a8ee 100644 --- a/examples/textures/textures_bunnymark_dynamic.py +++ b/examples/textures/textures_bunnymark_dynamic.py @@ -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: diff --git a/examples/textures/textures_bunnymark_more_pythonic.py b/examples/textures/textures_bunnymark_more_pythonic.py index 3a9e11f..2147651 100644 --- a/examples/textures/textures_bunnymark_more_pythonic.py +++ b/examples/textures/textures_bunnymark_more_pythonic.py @@ -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: @@ -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() #//---------------------------------------------------------------------------------- diff --git a/pyray/__init__.py b/pyray/__init__.py index a050c26..2629358 100644 --- a/pyray/__init__.py +++ b/pyray/__init__.py @@ -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 ) @@ -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)) == "": - 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)) == "" and str(result).startswith("": - 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 @@ -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) @@ -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)] +