diff --git a/crates/processing_core/src/error.rs b/crates/processing_core/src/error.rs index 59265767..f7ad743c 100644 --- a/crates/processing_core/src/error.rs +++ b/crates/processing_core/src/error.rs @@ -32,6 +32,10 @@ pub enum ProcessingError { TransformNotFound, #[error("Material not found")] MaterialNotFound, + #[error("Light not found")] + LightNotFound, + #[error("Light belongs to another graphics")] + LightOnOtherGraphics, #[error("Unknown shader property: {0}")] UnknownShaderProperty(String), #[error("GLTF load error: {0}")] diff --git a/crates/processing_ffi/src/lib.rs b/crates/processing_ffi/src/lib.rs index f78a8660..0acb0e1b 100644 --- a/crates/processing_ffi/src/lib.rs +++ b/crates/processing_ffi/src/lib.rs @@ -2856,6 +2856,18 @@ pub extern "C" fn processing_light_create_spot( .unwrap_or(0) } +#[unsafe(no_mangle)] +pub extern "C" fn processing_light(graphics_id: u64, light_id: u64) { + error::clear_error(); + error::check(|| graphics_light(Entity::from_bits(graphics_id), Entity::from_bits(light_id))); +} + +#[unsafe(no_mangle)] +pub extern "C" fn processing_light_destroy(light_id: u64) { + error::clear_error(); + error::check(|| light_destroy(Entity::from_bits(light_id))); +} + #[unsafe(no_mangle)] pub extern "C" fn processing_material_create_pbr() -> u64 { error::clear_error(); diff --git a/crates/processing_pyo3/examples/Basics/Camera/move_eye.py b/crates/processing_pyo3/examples/Basics/Camera/move_eye.py index eaf966da..1dd0e38d 100644 --- a/crates/processing_pyo3/examples/Basics/Camera/move_eye.py +++ b/crates/processing_pyo3/examples/Basics/Camera/move_eye.py @@ -4,19 +4,16 @@ # The camera lifts up (controlled by mouseY) while looking at the same point. from mewnala import * -light = None - def setup(): - global light size(640, 360) mode_3d() fill(0.8) - light = directional_light((1.0, 1.0, 1.0), 4000.0, position=(0, 0, 1), look_at=(0, 0, 0)) roughness(0.6) def draw(): + directional_light((1.0, 1.0, 1.0), 4000.0, position=(0, 0, 1), look_at=(0, 0, 0)) background(0.0) # Change height of the camera with mouseY (the world is y-up) diff --git a/crates/processing_pyo3/examples/Basics/Camera/orthographic.py b/crates/processing_pyo3/examples/Basics/Camera/orthographic.py index 2af88f60..e972367a 100644 --- a/crates/processing_pyo3/examples/Basics/Camera/orthographic.py +++ b/crates/processing_pyo3/examples/Basics/Camera/orthographic.py @@ -9,21 +9,19 @@ from mewnala import * show_perspective = False -light = None def setup(): - global light size(600, 360) mode_3d() no_fill() fill(1.0) no_stroke() - light = directional_light((1.0, 1.0, 1.0), 4000.0, position=(0, 0, 1), look_at=(0, 0, 0)) roughness(0.6) def draw(): + directional_light((1.0, 1.0, 1.0), 4000.0, position=(0, 0, 1), look_at=(0, 0, 0)) background(0.0) far = remap(mouse_x, 0, width, 120, 400) if show_perspective: diff --git a/crates/processing_pyo3/examples/Basics/Camera/perspective.py b/crates/processing_pyo3/examples/Basics/Camera/perspective.py index 1fa2a396..89ab953d 100644 --- a/crates/processing_pyo3/examples/Basics/Camera/perspective.py +++ b/crates/processing_pyo3/examples/Basics/Camera/perspective.py @@ -13,19 +13,16 @@ # to set the area precisely. from mewnala import * -light = None - def setup(): - global light size(640, 360) mode_3d() no_stroke() - light = directional_light((1.0, 1.0, 1.0), 4000.0, position=(0, 0, 1), look_at=(0, 0, 0)) roughness(0.6) def draw(): + directional_light((1.0, 1.0, 1.0), 4000.0, position=(0, 0, 1), look_at=(0, 0, 0)) background(0.0) camera_y = height / 2.0 # A fov of 0 would divide by zero below diff --git a/crates/processing_pyo3/examples/Basics/Form/primitives_3d.py b/crates/processing_pyo3/examples/Basics/Form/primitives_3d.py index 80a24356..9505446b 100644 --- a/crates/processing_pyo3/examples/Basics/Form/primitives_3d.py +++ b/crates/processing_pyo3/examples/Basics/Form/primitives_3d.py @@ -11,12 +11,12 @@ def setup(): size(640, 360) mode_3d() - directional_light((1.0, 1.0, 1.0), 4000.0, position=(0, 0, 1), look_at=(0, 0, 0)) roughness(0.6) no_loop() def draw(): + directional_light((1.0, 1.0, 1.0), 4000.0, position=(0, 0, 1), look_at=(0, 0, 0)) background(0.0) no_stroke() diff --git a/crates/processing_pyo3/examples/Basics/Lights/directional.py b/crates/processing_pyo3/examples/Basics/Lights/directional.py index fa55533a..4e0f4ad8 100644 --- a/crates/processing_pyo3/examples/Basics/Lights/directional.py +++ b/crates/processing_pyo3/examples/Basics/Lights/directional.py @@ -7,17 +7,13 @@ # scatters in all directions. from mewnala import * -light = None - def setup(): - global light size(640, 360) mode_3d() no_stroke() fill(0.8) - # Lights are created once; the shapes become lit once a material property is set - light = directional_light((0.8, 0.8, 0.8), 2500.0) + # The shapes become lit once a material property is set roughness(0.6) @@ -27,7 +23,7 @@ def draw(): dir_y = (mouse_y / height - 0.5) * 2 dir_x = (mouse_x / width - 0.5) * 2 # The world is y-up, so the vertical direction is flipped - light.look_at(-dir_x, dir_y, -1) + directional_light((0.8, 0.8, 0.8), 2500.0, look_at=(-dir_x, dir_y, -1)) translate(-100, 0, 0) sphere(80) translate(200, 0, 0) diff --git a/crates/processing_pyo3/examples/Basics/Lights/mixture.py b/crates/processing_pyo3/examples/Basics/Lights/mixture.py index 706ba2c2..0d63709e 100644 --- a/crates/processing_pyo3/examples/Basics/Lights/mixture.py +++ b/crates/processing_pyo3/examples/Basics/Lights/mixture.py @@ -4,37 +4,32 @@ # Display a box with three different kinds of lights. from mewnala import * -point = None -directional = None -spot = None - def setup(): - global point, directional, spot size(640, 360) mode_3d() no_stroke() + roughness(0.6) + + +def draw(): + background(0.0) - # Lights are created once; the world is y-up, so the y of every - # position and direction is flipped + # The world is y-up, so the y of every position and direction is flipped # Orange point light on the right - point = point_light((0.59, 0.39, 0.0), 3000000000.0, 600.0, 1.0, # Color - position=(200, 150, 0)) # Position + point_light((0.59, 0.39, 0.0), 3000000000.0, 600.0, 1.0, # Color + position=(200, 150, 0)) # Position # Blue directional light from the left - directional = directional_light((0.0, 0.4, 1.0), 4000.0, # Color - look_at=(1, 0, 0)) # The x-, y-, z-axis direction + directional_light((0.0, 0.4, 1.0), 4000.0, # Color + look_at=(1, 0, 0)) # The x-, y-, z-axis direction # Yellow spotlight from the front - spot = spot_light((1.0, 1.0, 0.43), 2000000000.0, 600.0, 1.0, # Color - PI / 4, PI / 2, # Angle, concentration - position=(0, -40, 200), # Position - look_at=(0, -40 + 0.5, 200 - 0.5)) # Direction - roughness(0.6) - + spot_light((1.0, 1.0, 0.43), 2000000000.0, 600.0, 1.0, # Color + PI / 4, PI / 2, # Angle, concentration + position=(0, -40, 200), # Position + look_at=(0, -40 + 0.5, 200 - 0.5)) # Direction -def draw(): - background(0.0) rotate_y(remap(mouse_x, 0, width, 0, PI)) rotate_x(remap(mouse_y, 0, height, 0, PI)) box(150) diff --git a/crates/processing_pyo3/examples/Basics/Lights/mixture_grid.py b/crates/processing_pyo3/examples/Basics/Lights/mixture_grid.py index 4ae00e16..51c23447 100644 --- a/crates/processing_pyo3/examples/Basics/Lights/mixture_grid.py +++ b/crates/processing_pyo3/examples/Basics/Lights/mixture_grid.py @@ -4,20 +4,17 @@ # Display a 2D grid of boxes with three different kinds of lights. from mewnala import * -point = None -directional = None -spot = None - def setup(): size(640, 360) mode_3d() no_stroke() - define_lights() + roughness(0.6) def draw(): background(0.0) + define_lights() for x in range(0, width + 1, 60): for y in range(0, height + 1, 60): @@ -30,23 +27,21 @@ def draw(): def define_lights(): - global point, directional, spot - # The lights are created once, placed relative to the top-left corner - # as in the original (the world is centered and y-up) + # The lights are placed relative to the top-left corner as in the + # original (the world is centered and y-up) # Orange point light on the right - point = point_light((0.59, 0.39, 0.0), 3000000000.0, 600.0, 1.0, # Color - position=(200 - width / 2, height / 2 + 150, 0)) # Position + point_light((0.59, 0.39, 0.0), 3000000000.0, 600.0, 1.0, # Color + position=(200 - width / 2, height / 2 + 150, 0)) # Position # Blue directional light from the left - directional = directional_light((0.0, 0.4, 1.0), 4000.0, # Color - look_at=(1, 0, 0)) # The x-, y-, z-axis direction + directional_light((0.0, 0.4, 1.0), 4000.0, # Color + look_at=(1, 0, 0)) # The x-, y-, z-axis direction # Yellow spotlight from the front - spot = spot_light((1.0, 1.0, 0.43), 2000000000.0, 600.0, 1.0, # Color - PI / 4, PI / 2, # Angle, concentration - position=(-width / 2, height / 2 - 40, 200), # Position - look_at=(-width / 2, height / 2 - 40 + 0.5, 200 - 0.5)) # Direction - roughness(0.6) + spot_light((1.0, 1.0, 0.43), 2000000000.0, 600.0, 1.0, # Color + PI / 4, PI / 2, # Angle, concentration + position=(-width / 2, height / 2 - 40, 200), # Position + look_at=(-width / 2, height / 2 - 40 + 0.5, 200 - 0.5)) # Direction run() diff --git a/crates/processing_pyo3/examples/Basics/Lights/on_off.py b/crates/processing_pyo3/examples/Basics/Lights/on_off.py index e29647cc..41ec5597 100644 --- a/crates/processing_pyo3/examples/Basics/Lights/on_off.py +++ b/crates/processing_pyo3/examples/Basics/Lights/on_off.py @@ -6,19 +6,17 @@ from mewnala import * spin = 0.0 -light = None def setup(): - global light size(640, 360) mode_3d() no_stroke() - light = directional_light((1.0, 1.0, 1.0), 4000.0, position=(0, 0, 1), look_at=(0, 0, 0)) def draw(): global spin + directional_light((1.0, 1.0, 1.0), 4000.0, position=(0, 0, 1), look_at=(0, 0, 0)) background(0.2) # A lit material turns the lights on; unlit() turns them off diff --git a/crates/processing_pyo3/examples/Basics/Lights/reflection.py b/crates/processing_pyo3/examples/Basics/Lights/reflection.py index 87797be0..6f224993 100644 --- a/crates/processing_pyo3/examples/Basics/Lights/reflection.py +++ b/crates/processing_pyo3/examples/Basics/Lights/reflection.py @@ -5,19 +5,16 @@ # with the horizontal position of the mouse. from mewnala import * -light = None - def setup(): - global light size(640, 360) mode_3d() no_stroke() fill(0.4) - light = directional_light((0.8, 0.8, 0.8), 4000.0, look_at=(0, 0, -1)) def draw(): + directional_light((0.8, 0.8, 0.8), 4000.0, look_at=(0, 0, -1)) background(0.0) # A smoother (less rough) material gives a stronger specular highlight s = mouse_x / width diff --git a/crates/processing_pyo3/examples/Basics/Lights/spot.py b/crates/processing_pyo3/examples/Basics/Lights/spot.py index 77c14c4a..1d63fbe7 100644 --- a/crates/processing_pyo3/examples/Basics/Lights/spot.py +++ b/crates/processing_pyo3/examples/Basics/Lights/spot.py @@ -4,40 +4,32 @@ # of a blue spot light. from mewnala import * -bottom = None -orange = None -blue = None - def setup(): - global bottom, orange, blue size(640, 360) mode_3d() no_stroke() fill(0.8) - - # Lights are created once and positioned relative to the top-left corner - # as in the original (the world is centered and y-up) - # Light the bottom of the sphere - bottom = directional_light((0.2, 0.4, 0.49), 4000.0, look_at=(0, 1, 0)) - - # Orange light on the upper-right of the sphere - orange = spot_light((0.8, 0.6, 0.0), 10000000000.0, 1200.0, 1.0, 0.0, PI / 12, - position=(360 - width / 2, height / 2 - 160, 600), - look_at=(360 - width / 2, height / 2 - 160, 0)) - - # Moving spotlight that follows the mouse - blue = spot_light((0.4, 0.6, 0.8), 10000000000.0, 1200.0, 1.0, 0.0, PI / 12, - position=(360 - width / 2, height / 2, 600), - look_at=(360 - width / 2, height / 2, 0)) roughness(0.6) def draw(): background(0.0) - blue.position(360 - width / 2, height / 2 - mouse_y, 600) - blue.look_at(360 - width / 2, height / 2 - mouse_y, 0) + # Lights are positioned relative to the top-left corner as in the + # original (the world is centered and y-up) + # Light the bottom of the sphere + directional_light((0.2, 0.4, 0.49), 4000.0, look_at=(0, 1, 0)) + + # Orange light on the upper-right of the sphere + spot_light((0.8, 0.6, 0.0), 10000000000.0, 1200.0, 1.0, 0.0, PI / 12, + position=(360 - width / 2, height / 2 - 160, 600), + look_at=(360 - width / 2, height / 2 - 160, 0)) + + # Moving spotlight that follows the mouse + spot_light((0.4, 0.6, 0.8), 10000000000.0, 1200.0, 1.0, 0.0, PI / 12, + position=(360 - width / 2, height / 2 - mouse_y, 600), + look_at=(360 - width / 2, height / 2 - mouse_y, 0)) sphere(120, 60, 60) diff --git a/crates/processing_pyo3/examples/Basics/Shape/load_display_obj.py b/crates/processing_pyo3/examples/Basics/Shape/load_display_obj.py index 3bc5cccb..6bdb9db3 100644 --- a/crates/processing_pyo3/examples/Basics/Shape/load_display_obj.py +++ b/crates/processing_pyo3/examples/Basics/Shape/load_display_obj.py @@ -14,12 +14,12 @@ def setup(): size(640, 360) mode_3d() rocket = load_model("data/rocket.obj") # GAP: load_model() (OBJ with .mtl and texture) is missing - directional_light((1.0, 1.0, 1.0), 4000.0, position=(0, 0, 1), look_at=(0, 0, 0)) roughness(0.6) def draw(): global ry + directional_light((1.0, 1.0, 1.0), 4000.0, position=(0, 0, 1), look_at=(0, 0, 0)) background(0.0) translate(0, -100, -200) diff --git a/crates/processing_pyo3/examples/Basics/Transform/rotate_push_pop.py b/crates/processing_pyo3/examples/Basics/Transform/rotate_push_pop.py index 6c3b2a48..17e83a1e 100644 --- a/crates/processing_pyo3/examples/Basics/Transform/rotate_push_pop.py +++ b/crates/processing_pyo3/examples/Basics/Transform/rotate_push_pop.py @@ -14,12 +14,12 @@ def setup(): size(640, 360) mode_3d() no_stroke() - directional_light((1.0, 1.0, 1.0), 4000.0, position=(0, 0, 1), look_at=(0, 0, 0)) roughness(0.6) def draw(): global a + directional_light((1.0, 1.0, 1.0), 4000.0, position=(0, 0, 1), look_at=(0, 0, 0)) background(0.0, 0.0, 0.1) for i in range(num): diff --git a/crates/processing_pyo3/examples/Demos/Graphics/patch.py b/crates/processing_pyo3/examples/Demos/Graphics/patch.py index a6fada8c..92679bee 100644 --- a/crates/processing_pyo3/examples/Demos/Graphics/patch.py +++ b/crates/processing_pyo3/examples/Demos/Graphics/patch.py @@ -21,13 +21,13 @@ def setup(): size(1024, 768) mode_3d() - # lights() - directional_light((1.0, 1.0, 1.0), 4000.0, position=(0, 0, 1), look_at=(0, 0, 0)) roughness(0.6) build() def draw(): + # lights() + directional_light((1.0, 1.0, 1.0), 4000.0, position=(0, 0, 1), look_at=(0, 0, 0)) background(1.0) scale(0.9) rotate_y(remap(mouse_x, 0, width, -PI, PI)) diff --git a/crates/processing_pyo3/examples/Demos/Graphics/planets.py b/crates/processing_pyo3/examples/Demos/Graphics/planets.py index e8a1177c..3bb371a4 100644 --- a/crates/processing_pyo3/examples/Demos/Graphics/planets.py +++ b/crates/processing_pyo3/examples/Demos/Graphics/planets.py @@ -11,12 +11,9 @@ cloudtex = None surftex2 = None -sun_light = None -planet_light = None - def setup(): - global starfield, suntex, surftex1, surftex2, sun_light, planet_light + global starfield, suntex, surftex1, surftex2 size(1024, 768) mode_3d() @@ -32,16 +29,16 @@ def setup(): no_stroke() fill(1.0) - # pointLight(255, 255, 255, 0, 0, 0) at the sun's position, and - # pointLight(255, 255, 255, 0, 0, -150) at the screen corner for planet1 - sun_light = point_light((1.0, 1.0, 1.0), 1000000.0, 1200.0, 1.0, position=(0, 0, -300)) - planet_light = point_light((1.0, 1.0, 1.0), 1000000.0, 1200.0, 1.0, position=(-width / 2, height / 2, -150)) - def draw(): # The star field is drawn as the background so it never occludes the 3D objects background(starfield) + # pointLight(255, 255, 255, 0, 0, 0) at the sun's position, and + # pointLight(255, 255, 255, 0, 0, -150) at the screen corner for planet1 + point_light((1.0, 1.0, 1.0), 1000000.0, 1200.0, 1.0, position=(0, 0, -300)) + point_light((1.0, 1.0, 1.0), 1000000.0, 1200.0, 1.0, position=(-width / 2, height / 2, -150)) + push_matrix() translate(0, 0, -300) diff --git a/crates/processing_pyo3/examples/Demos/Graphics/ribbons.py b/crates/processing_pyo3/examples/Demos/Graphics/ribbons.py index 9e354b86..7a516511 100644 --- a/crates/processing_pyo3/examples/Demos/Graphics/ribbons.py +++ b/crates/processing_pyo3/examples/Demos/Graphics/ribbons.py @@ -35,12 +35,12 @@ def setup(): arcball = Arcball(width / 2, height / 2, 600) read_pdb(pdb_file) - # ambient(80); lights() - directional_light((1.0, 1.0, 1.0), 4000.0, position=(0, 0, 1), look_at=(0, 0, 0)) roughness(0.6) def draw(): + # ambient(80); lights() + directional_light((1.0, 1.0, 1.0), 4000.0, position=(0, 0, 1), look_at=(0, 0, 0)) background(0.0) translate(0, 0, 200) diff --git a/crates/processing_pyo3/examples/Demos/Graphics/trefoil.py b/crates/processing_pyo3/examples/Demos/Graphics/trefoil.py index efbb181e..e744b15f 100644 --- a/crates/processing_pyo3/examples/Demos/Graphics/trefoil.py +++ b/crates/processing_pyo3/examples/Demos/Graphics/trefoil.py @@ -26,12 +26,12 @@ def setup(): # Saving trefoil surface into a geometry trefoil = create_trefoil(350, 60, 15) - # ambient(250, 250, 250); pointLight(255, 255, 255, 0, 0, 200) - point_light((1.0, 1.0, 1.0), 1000000.0, 600.0, 1.0, position=(0, 0, 200)) roughness(0.6) def draw(): + # ambient(250, 250, 250); pointLight(255, 255, 255, 0, 0, 200) + point_light((1.0, 1.0, 1.0), 1000000.0, 600.0, 1.0, position=(0, 0, 200)) background(0.0) pg.begin_draw() diff --git a/crates/processing_pyo3/examples/Topics/Geometry/icosahedra.py b/crates/processing_pyo3/examples/Topics/Geometry/icosahedra.py index 1d38d040..da5dd195 100644 --- a/crates/processing_pyo3/examples/Topics/Geometry/icosahedra.py +++ b/crates/processing_pyo3/examples/Topics/Geometry/icosahedra.py @@ -8,14 +8,12 @@ ico1 = None ico2 = None ico3 = None -light = None def setup(): - global ico1, ico2, ico3, light + global ico1, ico2, ico3 size(640, 360) mode_3d() - light = directional_light((1.0, 1.0, 1.0), 4000.0, position=(0, 0, 1), look_at=(0, 0, 0)) roughness(0.6) ico1 = Icosahedron(75) ico2 = Icosahedron(75) @@ -23,6 +21,7 @@ def setup(): def draw(): + directional_light((1.0, 1.0, 1.0), 4000.0, position=(0, 0, 1), look_at=(0, 0, 0)) background(0.0) push_matrix() diff --git a/crates/processing_pyo3/examples/Topics/Geometry/shape_transform.py b/crates/processing_pyo3/examples/Topics/Geometry/shape_transform.py index 525d5110..a8671cf4 100644 --- a/crates/processing_pyo3/examples/Topics/Geometry/shape_transform.py +++ b/crates/processing_pyo3/examples/Topics/Geometry/shape_transform.py @@ -21,23 +21,22 @@ is_pyramid = False angle_inc = 0 -light = None tube = None ends = None def setup(): - global angle_inc, light + global angle_inc size(640, 360) mode_3d() no_stroke() angle_inc = PI / 300.0 - light = directional_light((1.0, 1.0, 1.0), 4000.0, position=(0, 0, 1), look_at=(0, 0, 0)) roughness(0.6) build_shape() def draw(): + directional_light((1.0, 1.0, 1.0), 4000.0, position=(0, 0, 1), look_at=(0, 0, 0)) background(0.67, 0.37, 0.37) fill(1.0, 0.78, 0.78) rotate_x(frame_count * angle_inc) diff --git a/crates/processing_pyo3/examples/Topics/Geometry/space_junk.py b/crates/processing_pyo3/examples/Topics/Geometry/space_junk.py index c40bd77f..55bd7a6f 100644 --- a/crates/processing_pyo3/examples/Topics/Geometry/space_junk.py +++ b/crates/processing_pyo3/examples/Topics/Geometry/space_junk.py @@ -15,12 +15,8 @@ # Array for all cubes cubes = [] -blue_light = None -red_light = None - def setup(): - global blue_light, red_light size(640, 360) mode_3d() background(0.0) @@ -32,15 +28,6 @@ def setup(): int(random(-10, 10)), int(random(-140, 140)), int(random(-140, 140)), int(random(-140, 140)))) - # Set up some different colored lights, placed relative to the top-left - # corner as in the original (the world is centered and y-up) - blue_light = point_light((0.2, 0.4, 1.0), 3000000000.0, 600.0, 1.0, - position=(65 - width / 2, height / 2 - 60, 100)) - red_light = point_light((0.78, 0.16, 0.24), 3000000000.0, 600.0, 1.0, - position=(-65 - width / 2, height / 2 + 60, -150)) - - # Raise overall light in scene - ambient_light(0.27, 0.27, 0.04) # GAP: ambient_light() roughness(0.6) @@ -49,6 +36,16 @@ def draw(): background(0.0) fill(0.78) + # Set up some different colored lights, placed relative to the top-left + # corner as in the original (the world is centered and y-up) + point_light((0.2, 0.4, 1.0), 3000000000.0, 600.0, 1.0, + position=(65 - width / 2, height / 2 - 60, 100)) + point_light((0.78, 0.16, 0.24), 3000000000.0, 600.0, 1.0, + position=(-65 - width / 2, height / 2 + 60, -150)) + + # Raise overall light in scene + ambient_light(0.27, 0.27, 0.04) # GAP: ambient_light() + # Center geometry in display windwow. # you can changlee 3rd argument ('0') # to move block group closer(+) / further(-) diff --git a/crates/processing_pyo3/examples/Topics/Geometry/toroid.py b/crates/processing_pyo3/examples/Topics/Geometry/toroid.py index 64d7de98..b516c16c 100644 --- a/crates/processing_pyo3/examples/Topics/Geometry/toroid.py +++ b/crates/processing_pyo3/examples/Topics/Geometry/toroid.py @@ -37,21 +37,19 @@ is_helix = False helix_offset = 5.0 -light = None toroid = None def setup(): - global light size(640, 360) mode_3d() - # basic lighting setup - light = directional_light((1.0, 1.0, 1.0), 4000.0, position=(0, 0, 1), look_at=(0, 0, 0)) roughness(0.6) build_toroid() def draw(): + # basic lighting setup + directional_light((1.0, 1.0, 1.0), 4000.0, position=(0, 0, 1), look_at=(0, 0, 0)) background(0.2, 0.25, 0.16) # 2 rendering styles # wireframe or solid diff --git a/crates/processing_pyo3/examples/Topics/Geometry/vertices.py b/crates/processing_pyo3/examples/Topics/Geometry/vertices.py index 6bcc373c..d8f4ab1f 100644 --- a/crates/processing_pyo3/examples/Topics/Geometry/vertices.py +++ b/crates/processing_pyo3/examples/Topics/Geometry/vertices.py @@ -7,15 +7,13 @@ # number of sides drawn is variable. from mewnala import * -light = None cylinder = None def setup(): - global light, cylinder + global cylinder size(640, 360) mode_3d() - light = directional_light((1.0, 1.0, 1.0), 4000.0, position=(0, 0, 1), look_at=(0, 0, 0)) roughness(0.6) cylinder = build_cylinder(10, 180, 200, 16) # Build a mix between a cylinder and a cone # cylinder = build_cylinder(70, 70, 120, 64) # Build a cylinder @@ -23,6 +21,7 @@ def setup(): def draw(): + directional_light((1.0, 1.0, 1.0), 4000.0, position=(0, 0, 1), look_at=(0, 0, 0)) background(0.0) rotate_y(remap(mouse_x, 0, width, 0, PI)) rotate_z(remap(mouse_y, 0, height, 0, -PI)) diff --git a/crates/processing_pyo3/examples/Topics/Motion/cubes_within_cube.py b/crates/processing_pyo3/examples/Topics/Motion/cubes_within_cube.py index c857aafa..71d60092 100644 --- a/crates/processing_pyo3/examples/Topics/Motion/cubes_within_cube.py +++ b/crates/processing_pyo3/examples/Topics/Motion/cubes_within_cube.py @@ -21,11 +21,11 @@ def setup(): cubie_size = random(5, 15) cubies.append(Cube(cubie_size, cubie_size, cubie_size)) - directional_light((1.0, 1.0, 1.0), 4000.0, position=(0, 0, 1), look_at=(0, 0, 0)) roughness(0.6) def draw(): + directional_light((1.0, 1.0, 1.0), 4000.0, position=(0, 0, 1), look_at=(0, 0, 0)) background(0.2) # Center in display window diff --git a/crates/processing_pyo3/examples/Topics/Shaders/edge_filter.py b/crates/processing_pyo3/examples/Topics/Shaders/edge_filter.py index cf7c9066..210ed2c6 100644 --- a/crates/processing_pyo3/examples/Topics/Shaders/edge_filter.py +++ b/crates/processing_pyo3/examples/Topics/Shaders/edge_filter.py @@ -15,11 +15,11 @@ def setup(): mode_3d() edges = load_shader("data/edges.wesl") no_stroke() - directional_light((1.0, 1.0, 1.0), 4000.0, position=(0, 0, 1), look_at=(0, 0, 0)) roughness(0.6) def draw(): + directional_light((1.0, 1.0, 1.0), 4000.0, position=(0, 0, 1), look_at=(0, 0, 0)) background(0.0) push_matrix() diff --git a/crates/processing_pyo3/examples/Topics/Shaders/toon_shading.py b/crates/processing_pyo3/examples/Topics/Shaders/toon_shading.py index 2797a55c..f36aadd4 100644 --- a/crates/processing_pyo3/examples/Topics/Shaders/toon_shading.py +++ b/crates/processing_pyo3/examples/Topics/Shaders/toon_shading.py @@ -8,12 +8,11 @@ toon = None plain = None -light = None shader_enabled = True def setup(): - global toon, plain, light + global toon, plain size(640, 360) mode_3d() no_stroke() @@ -21,7 +20,6 @@ def setup(): toon = create_material(load_shader("data/toon.wesl"), light_dir=(0.0, 0.0, 1.0)) # the default (lit) material used when the toon shader is off plain = create_material(albedo=(0.8, 0.8, 0.8), roughness=0.6) - light = directional_light((0.8, 0.8, 0.8), 4000.0) def draw(): @@ -30,7 +28,7 @@ def draw(): dir_y = (mouse_y / height - 0.5) * 2 dir_x = (mouse_x / width - 0.5) * 2 # directionalLight(204, 204, 204, -dirX, -dirY, -1), y flipped for the y-up world - light.look_at(-dir_x, dir_y, -1) + directional_light((0.8, 0.8, 0.8), 4000.0, look_at=(-dir_x, dir_y, -1)) if shader_enabled: # the custom material does not see the scene lights: pass the # direction toward the light as a uniform diff --git a/crates/processing_pyo3/examples/Topics/Simulate/gravitational_attraction_3d.py b/crates/processing_pyo3/examples/Topics/Simulate/gravitational_attraction_3d.py index 8181baf0..589a5ee8 100644 --- a/crates/processing_pyo3/examples/Topics/Simulate/gravitational_attraction_3d.py +++ b/crates/processing_pyo3/examples/Topics/Simulate/gravitational_attraction_3d.py @@ -31,12 +31,12 @@ def setup(): planets.append(Planet(random(0.1, 2), random(-width / 2, width / 2), random(-height / 2, height / 2), random(-100, 100))) # A single sun s = Sun() - directional_light((1.0, 1.0, 1.0), 4000.0, position=(0, 0, 1), look_at=(0, 0, 0)) roughness(0.6) def draw(): global angle + directional_light((1.0, 1.0, 1.0), 4000.0, position=(0, 0, 1), look_at=(0, 0, 0)) background(0.0) # Setup the scene rotate_y(angle) diff --git a/crates/processing_pyo3/examples/camera_controllers.py b/crates/processing_pyo3/examples/camera_controllers.py index 562d5b63..f447a0c0 100644 --- a/crates/processing_pyo3/examples/camera_controllers.py +++ b/crates/processing_pyo3/examples/camera_controllers.py @@ -1,14 +1,16 @@ from mewnala import * angle = 0.0 -mode = 0 +mode = 0 +dir_light = None -def setup(): +def setup(): + global dir_light size(800, 600) mode_3d() orbit_camera() - dir_light = directional_light((1.0, 0.98, 0.95), 1500.0) + dir_light = create_directional_light((1.0, 0.98, 0.95), 1500.0) dir_light.position(300.0, 400.0, 300.0) dir_light.look_at(0.0, 0.0, 0.0) @@ -29,6 +31,7 @@ def draw(): mode = 2 background(13, 13, 18) + light(dir_light) if mode < 2: fill(255, 217, 145) roughness(0.3) diff --git a/crates/processing_pyo3/examples/flocking_duck.py b/crates/processing_pyo3/examples/flocking_duck.py index f53d1030..14fcc683 100644 --- a/crates/processing_pyo3/examples/flocking_duck.py +++ b/crates/processing_pyo3/examples/flocking_duck.py @@ -19,6 +19,7 @@ p = None boid = None mat = None +sun = None flock_pass = None integrate_pass = None center = None @@ -51,12 +52,12 @@ def boid_geometry(half_width, length, droop): def setup(): - global p, boid, mat, flock_pass, integrate_pass, center, extent, max_speed, boid_count + global p, boid, mat, sun, flock_pass, integrate_pass, center, extent, max_speed, boid_count size(900, 700) mode_3d() - directional_light((0.95, 0.9, 0.85), 800.0) + sun = create_directional_light((0.95, 0.9, 0.85), 800.0) gltf = load_gltf("gltf/Duck.glb") duck = gltf.geometry("LOD3spShape") @@ -124,6 +125,7 @@ def draw(): camera_position(center[0] + cos(t) * r, center[1] + extent * 0.35, center[2] + sin(t) * r) camera_look_at(center[0], center[1], center[2]) background(10, 12, 18) + light(sun) material(mat) particles(p, boid) diff --git a/crates/processing_pyo3/examples/flocking_gpu.py b/crates/processing_pyo3/examples/flocking_gpu.py index 4b33a5a8..01ac64cb 100644 --- a/crates/processing_pyo3/examples/flocking_gpu.py +++ b/crates/processing_pyo3/examples/flocking_gpu.py @@ -22,6 +22,7 @@ p = None boid = None mat = None +sun = None grid = None title_last_time = 0.0 title_last_frame = 0 @@ -49,13 +50,13 @@ def boid_geometry(half_width, length, droop): def setup(): - global p, boid, mat, grid + global p, boid, mat, sun, grid size(900, 700) window_title(f"GPU Flocking — {BOID_COUNT:,} boids") mode_3d() - directional_light((0.95, 0.9, 0.85), 800.0) + sun = create_directional_light((0.95, 0.9, 0.85), 800.0) p = create_particles(BOID_COUNT) @@ -134,6 +135,7 @@ def draw(): camera_position(cos(t) * r, BOUND * 0.8, sin(t) * r) camera_look_at(0.0, 0.0, 0.0) background(10, 12, 18) + light(sun) flock() diff --git a/crates/processing_pyo3/examples/gltf_load.py b/crates/processing_pyo3/examples/gltf_load.py index b96d16f9..9491aecb 100644 --- a/crates/processing_pyo3/examples/gltf_load.py +++ b/crates/processing_pyo3/examples/gltf_load.py @@ -4,11 +4,11 @@ gltf = None duck_geo = None duck_mat = None -light = None +lamp = None frame = 0 def setup(): - global gltf, duck_geo, duck_mat, light + global gltf, duck_geo, duck_mat, lamp size(800, 600) gltf = load_gltf("gltf/Duck.glb") @@ -17,7 +17,7 @@ def setup(): mode_3d() gltf.camera(0) - light = gltf.light(0) + lamp = gltf.light(0) def draw(): global frame @@ -27,8 +27,8 @@ def draw(): lx = math.cos(t) * radius ly = 1.5 lz = math.sin(t) * radius - light.position(lx, ly, lz) - light.look_at(0.0, 0.8, 0.0) + lamp.position(lx, ly, lz) + lamp.look_at(0.0, 0.8, 0.0) r = math.sin(t * 8.0) * 0.5 + 0.5 g = math.sin(t * 8.0 + 2.0) * 0.5 + 0.5 @@ -36,6 +36,7 @@ def draw(): duck_mat.set(base_color=[r, g, b, 1.0]) background(25) + light(lamp) material(duck_mat) draw_geometry(duck_geo) diff --git a/crates/processing_pyo3/examples/lights.py b/crates/processing_pyo3/examples/lights.py index f5d4d795..be7e879c 100644 --- a/crates/processing_pyo3/examples/lights.py +++ b/crates/processing_pyo3/examples/lights.py @@ -1,25 +1,30 @@ from mewnala import * angle = 0.0 +dir_light = None +point_light_a = None +point_light_b = None +spot_light_a = None def setup(): + global dir_light, point_light_a, point_light_b, spot_light_a size(800, 600) mode_3d() # Directional Light - dir_light = directional_light((0.5, 0.24, 1.0), 1500.0) + dir_light = create_directional_light((0.5, 0.24, 1.0), 1500.0) # Point Lights - point_light_a = point_light((1.0, 0.5, 0.25), 1000000.0, 200.0, 0.5) + point_light_a = create_point_light((1.0, 0.5, 0.25), 1000000.0, 200.0, 0.5) point_light_a.position(-25.0, 5.0, 51.0) point_light_a.look_at(0.0, 0.0, 0.0) - point_light_b = point_light((0.0, 0.5, 0.75), 2000000.0, 200.0, 0.25) + point_light_b = create_point_light((0.0, 0.5, 0.75), 2000000.0, 200.0, 0.25) point_light_b.position(0.0, 5.0, 50.5) point_light_b.look_at(0.0, 0.0, 0.0) # Spot Light - spot_light_a = spot_light((0.25, 0.8, 0.19), 15.0 * 1000000.0, 200.0, 0.84, 0.0, 0.7854) + spot_light_a = create_spot_light((0.25, 0.8, 0.19), 15.0 * 1000000.0, 200.0, 0.84, 0.0, 0.7854) spot_light_a.position(40.0, 0.0, 70.0) spot_light_a.look_at(0.0, 0.0, 0.0) @@ -28,6 +33,10 @@ def draw(): camera_position(100.0, 100.0, 300.0) camera_look_at(0.0, 0.0, 0.0) background(220) + light(dir_light) + light(point_light_a) + light(point_light_b) + light(spot_light_a) roughness(0.0) push_matrix() diff --git a/crates/processing_pyo3/examples/materials.py b/crates/processing_pyo3/examples/materials.py index 1924fb59..0d3a0712 100644 --- a/crates/processing_pyo3/examples/materials.py +++ b/crates/processing_pyo3/examples/materials.py @@ -1,14 +1,16 @@ from mewnala import * mat = None +dir_light = None +p_light = None def setup(): - global mat + global mat, dir_light, p_light size(800, 600) mode_3d() - dir_light = directional_light((1.0, 0.98, 0.95), 1500.0) - p_light = point_light((1.0, 1.0, 1.0), 100000.0, 800.0, 0.0) + dir_light = create_directional_light((1.0, 0.98, 0.95), 1500.0) + p_light = create_point_light((1.0, 1.0, 1.0), 100000.0, 800.0, 0.0) p_light.position(200.0, 200.0, 400.0) mat = create_material( @@ -21,6 +23,8 @@ def draw(): camera_position(0.0, 0.0, 200.0) camera_look_at(0.0, 0.0, 0.0) background(12, 12, 18) + light(dir_light) + light(p_light) material(mat) sphere(50.0) diff --git a/crates/processing_pyo3/examples/particles_animated.py b/crates/processing_pyo3/examples/particles_animated.py index 035df999..c3cc15ab 100644 --- a/crates/processing_pyo3/examples/particles_animated.py +++ b/crates/processing_pyo3/examples/particles_animated.py @@ -3,16 +3,17 @@ p = None sphere = None mat = None +sun = None spin = None def setup(): - global p, sphere, mat, spin + global p, sphere, mat, sun, spin size(900, 700) mode_3d() - directional_light((0.9, 0.85, 0.8), 300.0) + sun = create_directional_light((0.9, 0.85, 0.8), 300.0) sphere = Geometry.sphere(0.25, 12, 8) @@ -35,6 +36,7 @@ def draw(): camera_position(0.0, 8.0, 25.0) camera_look_at(0.0, 0.0, 0.0) background(15, 15, 20) + light(sun) fill(230, 128, 75) material(mat) diff --git a/crates/processing_pyo3/examples/particles_basic.py b/crates/processing_pyo3/examples/particles_basic.py index d94b64ec..02d1a533 100644 --- a/crates/processing_pyo3/examples/particles_basic.py +++ b/crates/processing_pyo3/examples/particles_basic.py @@ -3,15 +3,16 @@ p = None particle = None mat = None +sun = None def setup(): - global p, particle, mat + global p, particle, mat, sun size(900, 700) mode_3d() - directional_light((0.95, 0.9, 0.85), 600.0) + sun = create_directional_light((0.95, 0.9, 0.85), 600.0) source = Geometry.sphere(5.0, 32, 24) p = create_particles( @@ -36,6 +37,7 @@ def draw(): camera_position(0.0, 4.0, 18.0) camera_look_at(0.0, 0.0, 0.0) background(15, 15, 20) + light(sun) material(mat) particles(p, particle) diff --git a/crates/processing_pyo3/examples/particles_emit_gpu.py b/crates/processing_pyo3/examples/particles_emit_gpu.py index d32eb7e8..aa1bbc12 100644 --- a/crates/processing_pyo3/examples/particles_emit_gpu.py +++ b/crates/processing_pyo3/examples/particles_emit_gpu.py @@ -4,6 +4,7 @@ p = None particle = None mat = None +sun = None spawn = None motion = None @@ -16,12 +17,12 @@ def setup(): - global p, particle, mat, spawn, motion + global p, particle, mat, sun, spawn, motion size(900, 700) mode_3d() - directional_light((0.95, 0.9, 0.85), 800.0) + sun = create_directional_light((0.95, 0.9, 0.85), 800.0) particle = Geometry.sphere(0.12, 8, 6) @@ -50,6 +51,7 @@ def draw(): camera_position(0.0, 4.0, 16.0) camera_look_at(0.0, 2.0, 0.0) background(10, 10, 18) + light(sun) material(mat) particles(p, particle) diff --git a/crates/processing_pyo3/examples/particles_from_mesh.py b/crates/processing_pyo3/examples/particles_from_mesh.py index d577f41e..ceeb3dae 100644 --- a/crates/processing_pyo3/examples/particles_from_mesh.py +++ b/crates/processing_pyo3/examples/particles_from_mesh.py @@ -3,15 +3,16 @@ p = None particle = None mat = None +sun = None def setup(): - global p, particle, mat + global p, particle, mat, sun size(900, 700) mode_3d() - directional_light((0.95, 0.9, 0.85), 200.0) + sun = create_directional_light((0.95, 0.9, 0.85), 200.0) source = Geometry.sphere(5.0, 32, 24) p = create_particles( @@ -36,6 +37,7 @@ def draw(): camera_position(0.0, 4.0, 18.0) camera_look_at(0.0, 0.0, 0.0) background(15, 15, 20) + light(sun) material(mat) particles(p, particle) diff --git a/crates/processing_pyo3/examples/particles_noise.py b/crates/processing_pyo3/examples/particles_noise.py index 1d558e98..8e055bc5 100644 --- a/crates/processing_pyo3/examples/particles_noise.py +++ b/crates/processing_pyo3/examples/particles_noise.py @@ -3,16 +3,17 @@ p = None particle = None mat = None +sun = None noise = None def setup(): - global p, particle, mat, noise + global p, particle, mat, sun, noise size(900, 700) mode_3d() - directional_light((0.95, 0.9, 0.85), 200.0) + sun = create_directional_light((0.95, 0.9, 0.85), 200.0) source = Geometry.sphere(5.0, 32, 24) p = create_particles( @@ -38,6 +39,7 @@ def draw(): camera_position(0.0, 4.0, 18.0) camera_look_at(0.0, 0.0, 0.0) background(15, 15, 20) + light(sun) material(mat) particles(p, particle) diff --git a/crates/processing_pyo3/examples/particles_stress.py b/crates/processing_pyo3/examples/particles_stress.py index 68d7a8ff..147cad12 100644 --- a/crates/processing_pyo3/examples/particles_stress.py +++ b/crates/processing_pyo3/examples/particles_stress.py @@ -6,11 +6,14 @@ p = None cube = None +red_light = None +green_light = None +blue_light = None spin = None def setup(): - global p, cube, spin + global p, cube, spin, red_light, green_light, blue_light size(900, 700) mode_3d() @@ -20,9 +23,9 @@ def setup(): camera_look_at(0.0, 0.0, 0.0) orbit_camera() - directional_light((1.0, 0.0, 0.0), 1000.0, position=Vec3.X, look_at=Vec3.ZERO) - directional_light((0.0, 1.0, 0.0), 1000.0, position=Vec3.Y, look_at=Vec3.ZERO) - directional_light((0.0, 0.0, 1.0), 1000.0, position=Vec3.Z, look_at=Vec3.ZERO) + red_light = create_directional_light((1.0, 0.0, 0.0), 1000.0, position=Vec3.X, look_at=Vec3.ZERO) + green_light = create_directional_light((0.0, 1.0, 0.0), 1000.0, position=Vec3.Y, look_at=Vec3.ZERO) + blue_light = create_directional_light((0.0, 0.0, 1.0), 1000.0, position=Vec3.Z, look_at=Vec3.ZERO) p = create_particles( Geometry.grid(GRID, GRID, GRID, SPACING), @@ -45,6 +48,9 @@ def setup(): def draw(): background(10, 10, 18) + light(red_light) + light(green_light) + light(blue_light) particles(p, cube) p.apply(spin, rotation_axis=Vec3.Y, rotation_angle=SPIN_PER_FRAME) diff --git a/crates/processing_pyo3/examples/test_jupyter_3d.ipynb b/crates/processing_pyo3/examples/test_jupyter_3d.ipynb index 26c2ef7f..be9b11f5 100644 --- a/crates/processing_pyo3/examples/test_jupyter_3d.ipynb +++ b/crates/processing_pyo3/examples/test_jupyter_3d.ipynb @@ -22,11 +22,12 @@ "size(200, 100)\n", "mode_3d()\n", "\n", - "create_directional_light(1.0, 0.98, 0.95, 1500.0)\n", + "sun = create_directional_light((1.0, 0.98, 0.95), 1500.0)\n", "camera_position(0.0, 80.0, 200.0)\n", "camera_look_at(0.0, 0.0, 0.0)\n", "\n", "background(12, 12, 18)\n", + "light(sun)\n", "fill(255, 218, 145)\n", "metallic(0.8)\n", "roughness(0.3)\n", @@ -52,6 +53,7 @@ ], "source": [ "background(12, 12, 18)\n", + "light(sun)\n", "roughness(0.9)\n", "metallic(0.1)\n", "fill(100, 150, 255)\n", diff --git a/crates/processing_pyo3/src/graphics.rs b/crates/processing_pyo3/src/graphics.rs index 591fd911..3068944c 100644 --- a/crates/processing_pyo3/src/graphics.rs +++ b/crates/processing_pyo3/src/graphics.rs @@ -1,11 +1,11 @@ use crate::color::{ColorMode, extract_color_with_mode}; use crate::glfw::GlfwContext; use crate::input; -use crate::math::{extract_vec2, extract_vec3, extract_vec4}; +use crate::math::{Vec3Like, extract_vec2, extract_vec3, extract_vec4}; use bevy::{ color::{ColorToPacked, LinearRgba, Srgba}, math::{Affine3A, Mat4, Vec3, Vec4}, - prelude::Entity, + prelude::{Entity, Transform}, render::render_resource::{Extent3d, TextureFormat}, }; use processing::prelude::*; @@ -437,12 +437,30 @@ impl Light { } } -// TODO: implement `light_destroy` -// impl Drop for Light { -// fn drop(&mut self) { -// let _ = light_destroy(self.entity); -// } -// } +impl Light { + fn placed( + entity: Entity, + position: Option, + look_at: Option, + ) -> PyResult { + let light = Self { entity }; + if let Some(position) = position { + transform_set_position(light.entity, position.into_vec3()) + .map_err(|e| PyRuntimeError::new_err(format!("{e}")))?; + } + if let Some(target) = look_at { + transform_look_at(light.entity, target.into_vec3()) + .map_err(|e| PyRuntimeError::new_err(format!("{e}")))?; + } + Ok(light) + } +} + +impl Drop for Light { + fn drop(&mut self) { + let _ = light_destroy(self.entity); + } +} #[pyclass] #[derive(Debug)] @@ -2254,6 +2272,12 @@ impl Graphics { .map_err(|e| PyRuntimeError::new_err(format!("{e}"))) } + /// Adds a light to this frame. + pub fn light(&self, light: &Light) -> PyResult<()> { + graphics_light(self.entity, light.entity) + .map_err(|e| PyRuntimeError::new_err(format!("{e}"))) + } + pub fn material(&self, material: &crate::material::Material) -> PyResult<()> { graphics_record_command(self.entity, DrawCommand::Material(material.entity)) .map_err(|e| PyRuntimeError::new_err(format!("{e}"))) @@ -2544,30 +2568,125 @@ impl Graphics { camera_reset(self.entity).map_err(|e| PyRuntimeError::new_err(format!("{e}"))) } - pub fn light_directional( + /// Adds a directional light to this frame. + #[pyo3(signature = (color, illuminance, *, position=None, look_at=None))] + pub fn directional_light( + &self, + color: crate::color::ColorLike, + illuminance: f32, + position: Option, + look_at: Option, + ) -> PyResult<()> { + let kind = LightKind::Directional { + color: color.into_color()?, + illuminance, + }; + self.add_light(kind, position, look_at) + } + + /// Adds a point light to this frame. + #[pyo3(signature = (color, intensity, range, radius, *, position=None, look_at=None))] + pub fn point_light( + &self, + color: crate::color::ColorLike, + intensity: f32, + range: f32, + radius: f32, + position: Option, + look_at: Option, + ) -> PyResult<()> { + let kind = LightKind::Point { + color: color.into_color()?, + intensity, + range, + radius, + }; + self.add_light(kind, position, look_at) + } + + /// Adds a spot light to this frame. + #[pyo3(signature = (color, intensity, range, radius, inner_angle, outer_angle, *, position=None, look_at=None))] + #[allow(clippy::too_many_arguments)] + pub fn spot_light( + &self, + color: crate::color::ColorLike, + intensity: f32, + range: f32, + radius: f32, + inner_angle: f32, + outer_angle: f32, + position: Option, + look_at: Option, + ) -> PyResult<()> { + let kind = LightKind::Spot { + color: color.into_color()?, + intensity, + range, + radius, + inner_angle, + outer_angle, + }; + self.add_light(kind, position, look_at) + } + + /// Creates a directional light. + #[pyo3(signature = (color, illuminance, *, position=None, look_at=None))] + pub fn create_directional_light( &self, color: crate::color::ColorLike, illuminance: f32, + position: Option, + look_at: Option, ) -> PyResult { let color = color.into_color()?; - match light_create_directional(self.entity, color, illuminance) { - Ok(light) => Ok(Light { entity: light }), - Err(e) => Err(PyRuntimeError::new_err(format!("{e}"))), - } + let entity = light_create_directional(self.entity, color, illuminance) + .map_err(|e| PyRuntimeError::new_err(format!("{e}")))?; + Light::placed(entity, position, look_at) } - pub fn light_point( + /// Creates a point light. + #[pyo3(signature = (color, intensity, range, radius, *, position=None, look_at=None))] + pub fn create_point_light( &self, color: crate::color::ColorLike, intensity: f32, range: f32, radius: f32, + position: Option, + look_at: Option, ) -> PyResult { let color = color.into_color()?; - match light_create_point(self.entity, color, intensity, range, radius) { - Ok(light) => Ok(Light { entity: light }), - Err(e) => Err(PyRuntimeError::new_err(format!("{e}"))), - } + let entity = light_create_point(self.entity, color, intensity, range, radius) + .map_err(|e| PyRuntimeError::new_err(format!("{e}")))?; + Light::placed(entity, position, look_at) + } + + /// Creates a spot light. + #[pyo3(signature = (color, intensity, range, radius, inner_angle, outer_angle, *, position=None, look_at=None))] + #[allow(clippy::too_many_arguments)] + pub fn create_spot_light( + &self, + color: crate::color::ColorLike, + intensity: f32, + range: f32, + radius: f32, + inner_angle: f32, + outer_angle: f32, + position: Option, + look_at: Option, + ) -> PyResult { + let color = color.into_color()?; + let entity = light_create_spot( + self.entity, + color, + intensity, + range, + radius, + inner_angle, + outer_angle, + ) + .map_err(|e| PyRuntimeError::new_err(format!("{e}")))?; + Light::placed(entity, position, look_at) } #[getter] @@ -2629,29 +2748,24 @@ impl Graphics { fn key_is_pressed(&self) -> PyResult { input::key_is_pressed() } +} - pub fn light_spot( +impl Graphics { + fn add_light( &self, - color: crate::color::ColorLike, - intensity: f32, - range: f32, - radius: f32, - inner_angle: f32, - outer_angle: f32, - ) -> PyResult { - let color = color.into_color()?; - match light_create_spot( - self.entity, - color, - intensity, - range, - radius, - inner_angle, - outer_angle, - ) { - Ok(light) => Ok(Light { entity: light }), - Err(e) => Err(PyRuntimeError::new_err(format!("{e}"))), + kind: LightKind, + position: Option, + look_at: Option, + ) -> PyResult<()> { + let mut transform = Transform::default(); + if let Some(position) = position { + transform.translation = position.into_vec3(); + } + if let Some(target) = look_at { + transform.look_at(target.into_vec3(), Vec3::Y); } + graphics_immediate_light(self.entity, kind, transform) + .map_err(|e| PyRuntimeError::new_err(format!("{e}"))) } } diff --git a/crates/processing_pyo3/src/lib.rs b/crates/processing_pyo3/src/lib.rs index 01012978..d0bd02ef 100644 --- a/crates/processing_pyo3/src/lib.rs +++ b/crates/processing_pyo3/src/lib.rs @@ -1669,41 +1669,87 @@ pub mod mewnala { Shader::from_path(path) } - fn apply_light_transform( - light: &Light, + /// Adds a light to this frame. + #[pyfunction] + #[pyo3(pass_module, signature = (light))] + fn light(module: &Bound<'_, PyModule>, light: &Bound<'_, Light>) -> PyResult<()> { + graphics!(module).light(&*light.extract::>()?) + } + + /// Adds a directional light to this frame. + #[pyfunction] + #[pyo3(pass_module, signature = (color, illuminance, *, position=None, look_at=None))] + fn directional_light( + module: &Bound<'_, PyModule>, + color: super::color::ColorLike, + illuminance: f32, position: Option, look_at: Option, ) -> PyResult<()> { - if let Some(p) = position { - ::processing::prelude::transform_set_position(light.entity, p.into_vec3()) - .map_err(|e| PyRuntimeError::new_err(format!("{e}")))?; - } - if let Some(la) = look_at { - ::processing::prelude::transform_look_at(light.entity, la.into_vec3()) - .map_err(|e| PyRuntimeError::new_err(format!("{e}")))?; - } - Ok(()) + graphics!(module).directional_light(color, illuminance, position, look_at) } + /// Adds a point light to this frame. + #[pyfunction] + #[pyo3(pass_module, signature = (color, intensity, range, radius, *, position=None, look_at=None))] + fn point_light( + module: &Bound<'_, PyModule>, + color: super::color::ColorLike, + intensity: f32, + range: f32, + radius: f32, + position: Option, + look_at: Option, + ) -> PyResult<()> { + graphics!(module).point_light(color, intensity, range, radius, position, look_at) + } + + /// Adds a spot light to this frame. + #[pyfunction] + #[pyo3(pass_module, signature = (color, intensity, range, radius, inner_angle, outer_angle, *, position=None, look_at=None))] + #[allow(clippy::too_many_arguments)] + fn spot_light( + module: &Bound<'_, PyModule>, + color: super::color::ColorLike, + intensity: f32, + range: f32, + radius: f32, + inner_angle: f32, + outer_angle: f32, + position: Option, + look_at: Option, + ) -> PyResult<()> { + graphics!(module).spot_light( + color, + intensity, + range, + radius, + inner_angle, + outer_angle, + position, + look_at, + ) + } + + /// Creates a directional light. #[pyfunction] #[pyo3(pass_module, signature = (color, illuminance, *, position=None, look_at=None))] - fn directional_light( + fn create_directional_light( module: &Bound<'_, PyModule>, color: super::color::ColorLike, illuminance: f32, position: Option, look_at: Option, ) -> PyResult { - let graphics = - get_graphics(module)?.ok_or_else(|| PyRuntimeError::new_err("call size() first"))?; - let light = graphics.light_directional(color, illuminance)?; - apply_light_transform(&light, position, look_at)?; - Ok(light) + get_graphics(module)? + .ok_or_else(|| PyRuntimeError::new_err("call size() first"))? + .create_directional_light(color, illuminance, position, look_at) } + /// Creates a point light. #[pyfunction] #[pyo3(pass_module, signature = (color, intensity, range, radius, *, position=None, look_at=None))] - fn point_light( + fn create_point_light( module: &Bound<'_, PyModule>, color: super::color::ColorLike, intensity: f32, @@ -1712,16 +1758,16 @@ pub mod mewnala { position: Option, look_at: Option, ) -> PyResult { - let graphics = - get_graphics(module)?.ok_or_else(|| PyRuntimeError::new_err("call size() first"))?; - let light = graphics.light_point(color, intensity, range, radius)?; - apply_light_transform(&light, position, look_at)?; - Ok(light) + get_graphics(module)? + .ok_or_else(|| PyRuntimeError::new_err("call size() first"))? + .create_point_light(color, intensity, range, radius, position, look_at) } + /// Creates a spot light. #[pyfunction] #[pyo3(pass_module, signature = (color, intensity, range, radius, inner_angle, outer_angle, *, position=None, look_at=None))] - fn spot_light( + #[allow(clippy::too_many_arguments)] + fn create_spot_light( module: &Bound<'_, PyModule>, color: super::color::ColorLike, intensity: f32, @@ -1732,12 +1778,18 @@ pub mod mewnala { position: Option, look_at: Option, ) -> PyResult { - let graphics = - get_graphics(module)?.ok_or_else(|| PyRuntimeError::new_err("call size() first"))?; - let light = - graphics.light_spot(color, intensity, range, radius, inner_angle, outer_angle)?; - apply_light_transform(&light, position, look_at)?; - Ok(light) + get_graphics(module)? + .ok_or_else(|| PyRuntimeError::new_err("call size() first"))? + .create_spot_light( + color, + intensity, + range, + radius, + inner_angle, + outer_angle, + position, + look_at, + ) } #[pyfunction(name = "sphere")] diff --git a/crates/processing_render/src/gltf.rs b/crates/processing_render/src/gltf.rs index 7b9397be..1d0b57f0 100644 --- a/crates/processing_render/src/gltf.rs +++ b/crates/processing_render/src/gltf.rs @@ -6,7 +6,6 @@ use bevy::{ AssetPath, LoadState, handle_internal_asset_events, io::{AssetSourceId, embedded::GetAssetServer}, }, - camera::visibility::RenderLayers, ecs::system::RunSystemOnce, gltf::{Gltf, GltfMaterial, GltfMeshName}, pbr::ExtendedMaterial, @@ -16,7 +15,9 @@ use bevy::{ use crate::geometry::{BuiltinAttributes, Geometry, layout::VertexLayout}; use crate::graphics; +use crate::light::{LightOf, ProcessingLight}; use crate::material::ProcessingMaterial; +use crate::render::command::LightKind; use crate::render::material::{ProcessingExtendedMaterial, UntypedMaterial}; use processing_core::config::{Config, ConfigKey}; use processing_core::error::{ProcessingError, Result}; @@ -344,6 +345,7 @@ pub fn camera(In((gltf_entity, index)): In<(Entity, usize)>, world: &mut World) Ok(()) } +/// Creates a light from a light in the scene. pub fn light(In((gltf_entity, index)): In<(Entity, usize)>, world: &mut World) -> Result { let gltf_handle = world .get::(gltf_entity) @@ -355,11 +357,7 @@ pub fn light(In((gltf_entity, index)): In<(Entity, usize)>, world: &mut World) - let spawner = world.resource::(); spawner .iter_instance_entities(instance_id) - .filter(|&e| { - world.get::(e).is_some() - || world.get::(e).is_some() - || world.get::(e).is_some() - }) + .filter(|&e| scene_light_kind(world, e).is_some()) .collect() }; @@ -367,16 +365,35 @@ pub fn light(In((gltf_entity, index)): In<(Entity, usize)>, world: &mut World) - ProcessingError::GltfLoadError(format!("Light index {} not found", index)) })?; - let render_layers = world - .get::(graphics_entity) - .ok_or(ProcessingError::GraphicsNotFound)? - .clone(); - world.entity_mut(scene_light_entity).insert(render_layers); - + let kind = scene_light_kind(world, scene_light_entity).ok_or(ProcessingError::LightNotFound)?; let global = compute_global_transform(world, scene_light_entity); - *world - .get_mut::(scene_light_entity) - .ok_or(ProcessingError::GraphicsNotFound)? = global; + Ok(world + .spawn((ProcessingLight(kind), global, LightOf(graphics_entity))) + .id()) +} - Ok(scene_light_entity) +fn scene_light_kind(world: &World, entity: Entity) -> Option { + if let Some(light) = world.get::(entity) { + return Some(LightKind::Directional { + color: light.color, + illuminance: light.illuminance, + }); + } + if let Some(light) = world.get::(entity) { + return Some(LightKind::Point { + color: light.color, + intensity: light.intensity, + range: light.range, + radius: light.radius, + }); + } + let light = world.get::(entity)?; + Some(LightKind::Spot { + color: light.color, + intensity: light.intensity, + range: light.range, + radius: light.radius, + inner_angle: light.inner_angle, + outer_angle: light.outer_angle, + }) } diff --git a/crates/processing_render/src/graphics.rs b/crates/processing_render/src/graphics.rs index c8005e79..53a2d395 100644 --- a/crates/processing_render/src/graphics.rs +++ b/crates/processing_render/src/graphics.rs @@ -29,7 +29,7 @@ use crate::{ Flush, image::{Image, create_readback_buffer, pixel_size, pixels_to_bytes}, render::{ - BATCH_INDEX_STEP, RenderState, + BATCH_INDEX_STEP, DrawTransients, RenderState, command::{CommandBuffer, DrawCommand}, filter, }, @@ -529,11 +529,16 @@ pub fn destroy( Ok(()) } -pub fn begin_draw(In(entity): In, mut state_query: Query<&mut RenderState>) -> Result<()> { +pub fn begin_draw( + In(entity): In, + mut commands: Commands, + mut state_query: Query<&mut RenderState>, +) -> Result<()> { let mut state = state_query .get_mut(entity) .map_err(|_| ProcessingError::GraphicsNotFound)?; state.begin_frame(); + commands.entity(entity).despawn_related::(); Ok(()) } diff --git a/crates/processing_render/src/lib.rs b/crates/processing_render/src/lib.rs index 6bbfe67f..4b9a5d16 100644 --- a/crates/processing_render/src/lib.rs +++ b/crates/processing_render/src/lib.rs @@ -76,7 +76,7 @@ pub struct ProcessingRenderPlugin; impl Plugin for ProcessingRenderPlugin { fn build(&self, app: &mut App) { use render::material::{add_custom_materials, add_processing_materials}; - use render::{activate_cameras, clear_transient_meshes, flush_draw_commands}; + use render::{activate_cameras, clear_flush_transients, flush_draw_commands}; let config = app.world().resource::().clone(); @@ -108,7 +108,7 @@ impl Plugin for ProcessingRenderPlugin { )) .add_plugins(ci::CiPlugin); - app.add_systems(First, (clear_transient_meshes, activate_cameras)) + app.add_systems(First, (clear_flush_transients, activate_cameras)) .add_systems( Update, ( @@ -1359,14 +1359,10 @@ pub fn light_create_directional( color: Color, illuminance: f32, ) -> error::Result { - app_mut(|app| { - app.world_mut() - .run_system_cached_with( - light::create_directional, - (graphics_entity, color, illuminance), - ) - .unwrap() - }) + light_create( + graphics_entity, + render::command::LightKind::Directional { color, illuminance }, + ) } pub fn light_create_point( @@ -1376,14 +1372,15 @@ pub fn light_create_point( range: f32, radius: f32, ) -> error::Result { - app_mut(|app| { - app.world_mut() - .run_system_cached_with( - light::create_point, - (graphics_entity, color, intensity, range, radius), - ) - .unwrap() - }) + light_create( + graphics_entity, + render::command::LightKind::Point { + color, + intensity, + range, + radius, + }, + ) } pub fn light_create_spot( @@ -1395,24 +1392,76 @@ pub fn light_create_spot( inner_angle: f32, outer_angle: f32, ) -> error::Result { + light_create( + graphics_entity, + render::command::LightKind::Spot { + color, + intensity, + range, + radius, + inner_angle, + outer_angle, + }, + ) +} + +fn light_create( + graphics_entity: Entity, + kind: render::command::LightKind, +) -> error::Result { + app_mut(|app| { + app.world_mut() + .run_system_cached_with(light::create, (graphics_entity, kind, Transform::default())) + .unwrap() + }) +} + +/// Adds a light to the frame. +pub fn graphics_light(graphics_entity: Entity, light_entity: Entity) -> error::Result<()> { app_mut(|app| { + app.world_mut() + .run_system_cached_with(light::check_belongs_to, (light_entity, graphics_entity)) + .unwrap()?; app.world_mut() .run_system_cached_with( - light::create_spot, - ( - graphics_entity, - color, - intensity, - range, - radius, - inner_angle, - outer_angle, - ), + graphics::record_command, + (graphics_entity, DrawCommand::Light(light_entity)), ) .unwrap() }) } +/// Adds a light to the frame. +pub fn graphics_immediate_light( + graphics_entity: Entity, + kind: render::command::LightKind, + transform: Transform, +) -> error::Result<()> { + app_mut(|app| { + let light_entity = app + .world_mut() + .run_system_cached_with(light::create, (graphics_entity, kind, transform)) + .unwrap()?; + app.world_mut() + .entity_mut(light_entity) + .insert(render::DrawTransient(graphics_entity)); + app.world_mut() + .run_system_cached_with( + graphics::record_command, + (graphics_entity, DrawCommand::Light(light_entity)), + ) + .unwrap() + }) +} + +pub fn light_destroy(entity: Entity) -> error::Result<()> { + app_mut(|app| { + app.world_mut() + .run_system_cached_with(light::destroy, entity) + .unwrap() + }) +} + pub fn geometry_layout_create() -> error::Result { app_mut(|app| { Ok(app diff --git a/crates/processing_render/src/light.rs b/crates/processing_render/src/light.rs index 423703c2..077509f1 100644 --- a/crates/processing_render/src/light.rs +++ b/crates/processing_render/src/light.rs @@ -1,9 +1,12 @@ //! A light in Processing //! -use bevy::{camera::visibility::RenderLayers, prelude::*}; +use bevy::{camera::visibility::RenderLayers, math::Affine3A, prelude::*}; -use crate::{error::ProcessingError, graphics::Graphics}; +use crate::{ + error::ProcessingError, + render::{DrawTransient, RenderState, command::LightKind}, +}; pub struct LightPlugin; @@ -11,67 +14,68 @@ impl Plugin for LightPlugin { fn build(&self, _app: &mut App) {} } -pub fn create_directional( - In((entity, color, illuminance)): In<(Entity, Color, f32)>, - mut commands: Commands, - graphics: Query<&RenderLayers, With>, -) -> Result { - let layer = graphics - .get(entity) - .map_err(|_| ProcessingError::GraphicsNotFound)?; - Ok(commands - .spawn(( - DirectionalLight { - illuminance, - color, - ..default() - }, - layer.clone(), - )) - .id()) +/// A light, placed by its `Transform`. +#[derive(Component)] +pub struct ProcessingLight(pub LightKind); + +/// The graphics a light belongs to. +#[derive(Component)] +#[relationship(relationship_target = Lights)] +pub struct LightOf(pub Entity); + +/// A graphics' lights. +#[derive(Component, Default)] +#[relationship_target(relationship = LightOf, linked_spawn)] +pub struct Lights(Vec); + +/// Places a light's `Transform` with `matrix`. +pub fn place(matrix: Affine3A, transform: &Transform) -> Transform { + let position = matrix.transform_point3(transform.translation); + let forward = matrix.transform_vector3(*transform.forward()); + let up = matrix.transform_vector3(*transform.up()); + Transform::from_translation(position).looking_to(forward, up) } -pub fn create_point( - In((entity, color, intensity, range, radius)): In<(Entity, Color, f32, f32, f32)>, - mut commands: Commands, - graphics: Query<&RenderLayers, With>, -) -> Result { - let layer = graphics - .get(entity) - .map_err(|_| ProcessingError::GraphicsNotFound)?; - Ok(commands - .spawn(( - PointLight { - intensity, +/// Spawns the Bevy light for a light, lasting the rest of the graphics' draw. +pub(crate) fn spawn_bevy_light( + commands: &mut Commands, + graphics: Entity, + layers: &RenderLayers, + kind: LightKind, + transform: Transform, +) { + let mut entity = commands.spawn((DrawTransient(graphics), transform, layers.clone())); + match kind { + LightKind::Directional { color, illuminance } => { + entity.insert(DirectionalLight { color, + illuminance, + ..default() + }); + } + LightKind::Point { + color, + intensity, + range, + radius, + } => { + entity.insert(PointLight { + color, + intensity, range, radius, ..default() - }, - layer.clone(), - )) - .id()) -} - -pub fn create_spot( - In((entity, color, intensity, range, radius, inner_angle, outer_angle)): In<( - Entity, - Color, - f32, - f32, - f32, - f32, - f32, - )>, - mut commands: Commands, - graphics: Query<&RenderLayers, With>, -) -> Result { - let layer = graphics - .get(entity) - .map_err(|_| ProcessingError::GraphicsNotFound)?; - Ok(commands - .spawn(( - SpotLight { + }); + } + LightKind::Spot { + color, + intensity, + range, + radius, + inner_angle, + outer_angle, + } => { + entity.insert(SpotLight { color, intensity, range, @@ -79,8 +83,214 @@ pub fn create_spot( inner_angle, outer_angle, ..default() - }, - layer.clone(), - )) + }); + } + } +} + +pub fn create( + In((graphics, kind, transform)): In<(Entity, LightKind, Transform)>, + mut commands: Commands, + graphics_query: Query<(), With>, +) -> Result { + if !graphics_query.contains(graphics) { + return Err(ProcessingError::GraphicsNotFound); + } + Ok(commands + .spawn((ProcessingLight(kind), transform, LightOf(graphics))) .id()) } + +pub fn check_belongs_to( + In((light, graphics)): In<(Entity, Entity)>, + lights: Query<&LightOf, With>, +) -> Result<(), ProcessingError> { + let owner = lights + .get(light) + .map_err(|_| ProcessingError::LightNotFound)?; + if owner.0 != graphics { + return Err(ProcessingError::LightOnOtherGraphics); + } + Ok(()) +} + +pub fn destroy( + In(entity): In, + mut commands: Commands, + lights: Query<(), With>, +) -> Result<(), ProcessingError> { + if !lights.contains(entity) { + return Err(ProcessingError::LightNotFound); + } + commands.entity(entity).despawn(); + Ok(()) +} + +#[cfg(test)] +mod tests { + use std::f32::consts::FRAC_PI_2; + + use bevy::ecs::{relationship::RelationshipTarget, system::RunSystemOnce}; + + use super::*; + use crate::{graphics::begin_draw, render::DrawTransients}; + + const DIRECTIONAL: LightKind = LightKind::Directional { + color: Color::WHITE, + illuminance: 1000.0, + }; + + const POINT: LightKind = LightKind::Point { + color: Color::WHITE, + intensity: 1000.0, + range: 10.0, + radius: 0.0, + }; + + fn create_light(world: &mut World, graphics: Entity, kind: LightKind) -> Entity { + let light = world + .run_system_once_with(create, (graphics, kind, Transform::default())) + .unwrap() + .unwrap(); + world.flush(); + light + } + + fn spawn_lights(world: &mut World, graphics: Entity, kinds: Vec) -> Vec { + world + .run_system_once(move |mut commands: Commands| { + for &kind in &kinds { + let layers = RenderLayers::layer(1); + spawn_bevy_light(&mut commands, graphics, &layers, kind, Transform::default()); + } + }) + .unwrap(); + world + .get::(graphics) + .map(|t| t.iter().collect()) + .unwrap_or_default() + } + + #[test] + fn each_light_spawns_a_bevy_light() { + let mut world = World::new(); + let graphics = world.spawn_empty().id(); + let spawned = spawn_lights(&mut world, graphics, vec![DIRECTIONAL, POINT]); + assert_eq!(spawned.len(), 2); + assert!(world.get::(spawned[0]).is_some()); + assert!(world.get::(spawned[1]).is_some()); + } + + #[test] + fn the_next_draw_clears_spawned_lights() { + let mut world = World::new(); + let graphics = world.spawn(RenderState::new()).id(); + let spawned = spawn_lights(&mut world, graphics, vec![DIRECTIONAL]); + world + .run_system_once_with(begin_draw, graphics) + .unwrap() + .unwrap(); + assert!(world.get_entity(spawned[0]).is_err()); + } + + #[test] + fn a_created_light_is_not_a_bevy_light() { + let mut world = World::new(); + let graphics = world.spawn(RenderState::new()).id(); + let light = create_light(&mut world, graphics, DIRECTIONAL); + assert!(world.get::(light).is_none()); + } + + #[test] + fn lights_are_despawned_with_their_graphics() { + let mut world = World::new(); + let graphics = world.spawn(RenderState::new()).id(); + let light = create_light(&mut world, graphics, DIRECTIONAL); + let transform = Transform::from_xyz(1.0, 2.0, 3.0); + let immediate = world + .run_system_once_with(create, (graphics, POINT, transform)) + .unwrap() + .unwrap(); + world.flush(); + assert_eq!(*world.get::(immediate).unwrap(), transform); + world.despawn(graphics); + assert!(world.get_entity(light).is_err()); + assert!(world.get_entity(immediate).is_err()); + } + + #[test] + fn a_light_needs_a_graphics() { + let mut world = World::new(); + let other = world.spawn_empty().id(); + let created = world + .run_system_once_with(create, (other, POINT, Transform::default())) + .unwrap(); + assert!(matches!(created, Err(ProcessingError::GraphicsNotFound))); + let immediate = world + .run_system_once_with(create, (other, POINT, Transform::default())) + .unwrap(); + assert!(matches!(immediate, Err(ProcessingError::GraphicsNotFound))); + } + + #[test] + fn a_light_only_belongs_to_the_graphics_that_made_it() { + let mut world = World::new(); + let graphics = world.spawn(RenderState::new()).id(); + let other_graphics = world.spawn(RenderState::new()).id(); + let light = create_light(&mut world, graphics, DIRECTIONAL); + let own = world + .run_system_once_with(check_belongs_to, (light, graphics)) + .unwrap(); + assert!(own.is_ok()); + let other = world + .run_system_once_with(check_belongs_to, (light, other_graphics)) + .unwrap(); + assert!(matches!(other, Err(ProcessingError::LightOnOtherGraphics))); + let not_a_light = world + .run_system_once_with(check_belongs_to, (other_graphics, graphics)) + .unwrap(); + assert!(matches!(not_a_light, Err(ProcessingError::LightNotFound))); + } + + #[test] + fn destroy_despawns_a_light() { + let mut world = World::new(); + let graphics = world.spawn(RenderState::new()).id(); + let light = create_light(&mut world, graphics, DIRECTIONAL); + let result = world.run_system_once_with(destroy, light).unwrap(); + assert!(result.is_ok()); + assert!(world.get_entity(light).is_err()); + } + + #[test] + fn destroy_rejects_an_entity_that_is_not_a_light() { + let mut world = World::new(); + let other = world.spawn_empty().id(); + let result = world.run_system_once_with(destroy, other).unwrap(); + assert!(matches!(result, Err(ProcessingError::LightNotFound))); + assert!(world.get_entity(other).is_ok()); + } + + #[test] + fn identity_keeps_the_placement() { + let transform = Transform::from_xyz(0.0, 0.0, 1.0).looking_at(Vec3::ZERO, Vec3::Y); + let placed = place(Affine3A::IDENTITY, &transform); + assert!(placed.translation.abs_diff_eq(transform.translation, 1e-5)); + assert!(placed.rotation.abs_diff_eq(transform.rotation, 1e-5)); + } + + #[test] + fn current_matrix_moves_the_light() { + let matrix = Affine3A::from_translation(Vec3::new(10.0, 0.0, 0.0)); + let placed = place(matrix, &Transform::from_xyz(0.0, 5.0, 0.0)); + assert_eq!(placed.translation, Vec3::new(10.0, 5.0, 0.0)); + } + + #[test] + fn current_matrix_rotates_the_light_without_scaling_it() { + let matrix = Affine3A::from_scale(Vec3::splat(2.0)) * Affine3A::from_rotation_y(FRAC_PI_2); + let placed = place(matrix, &Transform::default()); + assert!(placed.scale.abs_diff_eq(Vec3::ONE, 1e-5)); + assert!(placed.forward().dot(Vec3::NEG_X) > 0.999); + } +} diff --git a/crates/processing_render/src/render/command.rs b/crates/processing_render/src/render/command.rs index 7efecf56..7e19f613 100644 --- a/crates/processing_render/src/render/command.rs +++ b/crates/processing_render/src/render/command.rs @@ -499,8 +499,31 @@ impl BlendMode { } } +#[derive(Debug, Clone, Copy)] +pub enum LightKind { + Directional { + color: Color, + illuminance: f32, + }, + Point { + color: Color, + intensity: f32, + range: f32, + radius: f32, + }, + Spot { + color: Color, + intensity: f32, + range: f32, + radius: f32, + inner_angle: f32, + outer_angle: f32, + }, +} + #[derive(Debug, Clone)] pub enum DrawCommand { + Light(Entity), BackgroundColor(Color), BackgroundImage(Entity), Fill(Color), diff --git a/crates/processing_render/src/render/mod.rs b/crates/processing_render/src/render/mod.rs index 0e797032..d642c0e5 100644 --- a/crates/processing_render/src/render/mod.rs +++ b/crates/processing_render/src/render/mod.rs @@ -33,6 +33,7 @@ use crate::{ geometry::Geometry, gltf::GltfNodeTransform, image::Image, + light::ProcessingLight, material::ProcessingMaterial, material::custom::CustomMaterial, particles::{Particles, ParticlesDraw}, @@ -42,13 +43,23 @@ use crate::{ pub(crate) const BATCH_INDEX_STEP: f32 = 0.001; +/// Despawned at the graphics' next flush. #[derive(Component)] -#[relationship(relationship_target = TransientMeshes)] -pub struct BelongsToGraphics(pub Entity); +#[relationship(relationship_target = FlushTransients)] +pub struct FlushTransient(pub Entity); #[derive(Component, Default)] -#[relationship_target(relationship = BelongsToGraphics, linked_spawn)] -pub struct TransientMeshes(Vec); +#[relationship_target(relationship = FlushTransient, linked_spawn)] +pub struct FlushTransients(Vec); + +/// Despawned when the graphics begins its next draw. +#[derive(Component)] +#[relationship(relationship_target = DrawTransients)] +pub struct DrawTransient(pub Entity); + +#[derive(Component, Default)] +#[relationship_target(relationship = DrawTransient, linked_spawn)] +pub struct DrawTransients(Vec); #[derive(SystemParam)] pub struct RenderResources<'w, 's> { @@ -160,6 +171,7 @@ pub fn flush_draw_commands( With, )>, >, + p_lights: Query<(&ProcessingLight, &Transform)>, ) { for (graphics_entity, mut cmd_buffer, mut state, render_layers, projection, camera_transform) in graphics.iter_mut() @@ -180,6 +192,18 @@ pub fn flush_draw_commands( for cmd in draw_commands { match cmd { + DrawCommand::Light(entity) => { + if let Ok((light, transform)) = p_lights.get(entity) { + let placed = crate::light::place(state.transform.current(), transform); + crate::light::spawn_bevy_light( + &mut res.commands, + graphics_entity, + render_layers, + light.0, + placed, + ); + } + } DrawCommand::Fill(color) => { state.style.fill = Fill::Color(color); } @@ -863,7 +887,7 @@ pub fn flush_draw_commands( res.commands.spawn(( Mesh3d(mesh_handle), UntypedMaterial(material_handle), - BelongsToGraphics(batch.graphics_entity), + FlushTransient(batch.graphics_entity), Transform::IDENTITY, batch.render_layers.clone(), )); @@ -892,7 +916,7 @@ pub fn flush_draw_commands( res.commands.spawn(( Mesh3d(mesh_handle), UntypedMaterial(material_handle), - BelongsToGraphics(batch.graphics_entity), + FlushTransient(batch.graphics_entity), Transform::IDENTITY, batch.render_layers.clone(), )); @@ -954,7 +978,7 @@ pub fn flush_draw_commands( res.commands.spawn(( Mesh3d(geometry.handle.clone()), UntypedMaterial(material_handle), - BelongsToGraphics(batch.graphics_entity), + FlushTransient(batch.graphics_entity), transform, batch.render_layers.clone(), )); @@ -1401,13 +1425,13 @@ pub fn activate_cameras(mut cameras: Query<(&mut Camera, Option<&Flush>)>) { } } -pub fn clear_transient_meshes( +pub fn clear_flush_transients( mut commands: Commands, - surfaces: Query<&TransientMeshes, With>, + surfaces: Query<&FlushTransients, With>, ) { - for transient_meshes in surfaces.iter() { - for &mesh_entity in transient_meshes.0.iter() { - commands.entity(mesh_entity).despawn(); + for transients in surfaces.iter() { + for &entity in transients.0.iter() { + commands.entity(entity).despawn(); } } } @@ -1449,7 +1473,7 @@ fn spawn_mesh( res.commands.spawn(( Mesh3d(mesh_handle), UntypedMaterial(material_handle), - BelongsToGraphics(batch.graphics_entity), + FlushTransient(batch.graphics_entity), transform, batch.render_layers.clone(), )); @@ -1683,7 +1707,7 @@ fn add_shape3d( let mut entity = res.commands.spawn(( Mesh3d(mesh_handle), UntypedMaterial(material_handle), - BelongsToGraphics(batch.graphics_entity), + FlushTransient(batch.graphics_entity), transform, batch.render_layers.clone(), )); diff --git a/crates/processing_wasm/src/lib.rs b/crates/processing_wasm/src/lib.rs index ec74ce39..3153b783 100644 --- a/crates/processing_wasm/src/lib.rs +++ b/crates/processing_wasm/src/lib.rs @@ -1666,6 +1666,19 @@ pub fn js_light_create_spot( .map(|e| e.to_bits()) } +#[wasm_bindgen(js_name = "light")] +pub fn js_light(graphics_id: u64, light_id: u64) -> Result<(), JsValue> { + check(graphics_light( + Entity::from_bits(graphics_id), + Entity::from_bits(light_id), + )) +} + +#[wasm_bindgen(js_name = "lightDestroy")] +pub fn js_light_destroy(light_id: u64) -> Result<(), JsValue> { + check(light_destroy(Entity::from_bits(light_id))) +} + #[wasm_bindgen(js_name = "materialCreatePbr")] pub fn js_material_create_pbr() -> Result { check(material_create_pbr()).map(|e| e.to_bits()) diff --git a/docs/api.md b/docs/api.md index 21511fd5..ef760330 100644 --- a/docs/api.md +++ b/docs/api.md @@ -36,7 +36,11 @@ not guaranteed to be the same as a user-created image. ### Light -[//]: # (TODO: Document Image API object) +`directional_light`, `point_light` and `spot_light` add a light to the frame. `create_directional_light`, +`create_point_light` and `create_spot_light` return a `Light` that `light(l)` adds to the frame. Both use the current +matrix. A `Light` belongs to the graphics that created it. + +A light also lights shapes drawn before it in the frame, unless a flush (such as `filter`) came between them. ### Image diff --git a/examples/camera_controllers.rs b/examples/camera_controllers.rs index 187ceef7..fa4c80f4 100644 --- a/examples/camera_controllers.rs +++ b/examples/camera_controllers.rs @@ -56,6 +56,7 @@ fn sketch() -> error::Result<()> { } graphics_begin_draw(graphics)?; + graphics_light(graphics, dir_light)?; graphics_record_command( graphics, diff --git a/examples/gltf_load.rs b/examples/gltf_load.rs index af9d38c5..c9976a32 100644 --- a/examples/gltf_load.rs +++ b/examples/gltf_load.rs @@ -53,6 +53,7 @@ fn sketch() -> error::Result<()> { material_set(duck_mat, "base_color", ShaderValue::Float4([r, g, b, 1.0]))?; graphics_begin_draw(graphics)?; + graphics_light(graphics, light)?; graphics_record_command( graphics, diff --git a/examples/lights.rs b/examples/lights.rs index 224cbf86..a0e6511f 100644 --- a/examples/lights.rs +++ b/examples/lights.rs @@ -29,11 +29,8 @@ fn sketch() -> error::Result<()> { let pbr_mat = material_create_pbr()?; material_set(pbr_mat, "roughness", shader_value::ShaderValue::Float(0.0))?; - // We will only declare lights in `setup` - // rather than calling some sort of `light()` method inside of `draw` - // create a directional light - let _dir_light = + let dir_light = light_create_directional(graphics, bevy::color::Color::srgb(0.35, 0.25, 0.5), 500.0)?; // create a point light @@ -79,6 +76,10 @@ fn sketch() -> error::Result<()> { while glfw_ctx.poll_events() { graphics_begin_draw(graphics)?; + graphics_light(graphics, dir_light)?; + graphics_light(graphics, point_light_a)?; + graphics_light(graphics, point_light_b)?; + graphics_light(graphics, spot_light)?; graphics_record_command( graphics, diff --git a/examples/materials.rs b/examples/materials.rs index c3805133..ea542441 100644 --- a/examples/materials.rs +++ b/examples/materials.rs @@ -68,6 +68,8 @@ fn sketch() -> error::Result<()> { while glfw_ctx.poll_events() { graphics_begin_draw(graphics)?; + graphics_light(graphics, dir_light)?; + graphics_light(graphics, point_light)?; graphics_record_command( graphics, diff --git a/examples/particles_animated.rs b/examples/particles_animated.rs index fe22240c..9a26ce2e 100644 --- a/examples/particles_animated.rs +++ b/examples/particles_animated.rs @@ -44,7 +44,7 @@ fn sketch() -> error::Result<()> { transform_set_position(graphics, Vec3::new(0.0, 8.0, 25.0))?; transform_look_at(graphics, Vec3::new(0.0, 0.0, 0.0))?; - let _light = + let light = light_create_directional(graphics, bevy::color::Color::srgb(0.9, 0.85, 0.8), 300.0)?; let sphere = geometry_sphere(0.25, 12, 8)?; @@ -76,6 +76,7 @@ fn sketch() -> error::Result<()> { while glfw_ctx.poll_events() { graphics_begin_draw(graphics)?; + graphics_light(graphics, light)?; graphics_record_command( graphics, DrawCommand::BackgroundColor(bevy::color::Color::srgb(0.06, 0.06, 0.08)), diff --git a/examples/particles_basic.rs b/examples/particles_basic.rs index 34c1220f..2a383fc8 100644 --- a/examples/particles_basic.rs +++ b/examples/particles_basic.rs @@ -20,7 +20,7 @@ fn sketch() -> error::Result<()> { transform_set_position(graphics, Vec3::new(0.0, 0.0, 25.0))?; transform_look_at(graphics, Vec3::new(0.0, 0.0, 0.0))?; - let _light = + let light = light_create_directional(graphics, bevy::color::Color::srgb(0.9, 0.85, 0.8), 300.0)?; let sphere = geometry_sphere(0.25, 12, 8)?; @@ -49,6 +49,7 @@ fn sketch() -> error::Result<()> { while glfw_ctx.poll_events() { graphics_begin_draw(graphics)?; + graphics_light(graphics, light)?; graphics_record_command( graphics, DrawCommand::BackgroundColor(bevy::color::Color::srgb(0.06, 0.06, 0.08)), diff --git a/examples/particles_colored_pbr.rs b/examples/particles_colored_pbr.rs index b61311ae..35c10abf 100644 --- a/examples/particles_colored_pbr.rs +++ b/examples/particles_colored_pbr.rs @@ -20,7 +20,7 @@ fn sketch() -> error::Result<()> { transform_set_position(graphics, Vec3::new(0.0, 6.0, 18.0))?; transform_look_at(graphics, Vec3::new(0.0, 0.0, 0.0))?; - let _light = + let light = light_create_directional(graphics, bevy::color::Color::srgb(0.95, 0.9, 0.85), 200.0)?; let sphere = geometry_sphere(0.3, 16, 12)?; @@ -66,6 +66,7 @@ fn sketch() -> error::Result<()> { while glfw_ctx.poll_events() { graphics_begin_draw(graphics)?; + graphics_light(graphics, light)?; graphics_record_command( graphics, DrawCommand::BackgroundColor(bevy::color::Color::srgb(0.06, 0.06, 0.08)), diff --git a/examples/particles_emit_gpu.rs b/examples/particles_emit_gpu.rs index 74565dc3..81fde6ca 100644 --- a/examples/particles_emit_gpu.rs +++ b/examples/particles_emit_gpu.rs @@ -130,7 +130,7 @@ fn sketch() -> error::Result<()> { transform_set_position(graphics, Vec3::new(0.0, 4.0, 16.0))?; transform_look_at(graphics, Vec3::new(0.0, 2.0, 0.0))?; - let _light = + let light = light_create_directional(graphics, bevy::color::Color::srgb(0.95, 0.9, 0.85), 800.0)?; let particle = geometry_sphere(0.12, 8, 6)?; @@ -178,6 +178,7 @@ fn sketch() -> error::Result<()> { while glfw_ctx.poll_events() { graphics_begin_draw(graphics)?; + graphics_light(graphics, light)?; graphics_record_command( graphics, DrawCommand::BackgroundColor(bevy::color::Color::srgb(0.04, 0.04, 0.07)), diff --git a/examples/particles_from_mesh.rs b/examples/particles_from_mesh.rs index 8d03f4d4..eaaaeee0 100644 --- a/examples/particles_from_mesh.rs +++ b/examples/particles_from_mesh.rs @@ -20,7 +20,7 @@ fn sketch() -> error::Result<()> { transform_set_position(graphics, Vec3::new(0.0, 4.0, 18.0))?; transform_look_at(graphics, Vec3::new(0.0, 0.0, 0.0))?; - let _light = + let light = light_create_directional(graphics, bevy::color::Color::srgb(0.95, 0.9, 0.85), 200.0)?; let source = geometry_sphere(5.0, 32, 24)?; @@ -56,6 +56,7 @@ fn sketch() -> error::Result<()> { while glfw_ctx.poll_events() { graphics_begin_draw(graphics)?; + graphics_light(graphics, light)?; graphics_record_command( graphics, DrawCommand::BackgroundColor(bevy::color::Color::srgb(0.06, 0.06, 0.08)), diff --git a/examples/particles_noise.rs b/examples/particles_noise.rs index 71baabc3..ab61542d 100644 --- a/examples/particles_noise.rs +++ b/examples/particles_noise.rs @@ -20,7 +20,7 @@ fn sketch() -> error::Result<()> { transform_set_position(graphics, Vec3::new(0.0, 4.0, 18.0))?; transform_look_at(graphics, Vec3::new(0.0, 0.0, 0.0))?; - let _light = + let light = light_create_directional(graphics, bevy::color::Color::srgb(0.95, 0.9, 0.85), 200.0)?; let source = geometry_sphere(5.0, 32, 24)?; @@ -54,6 +54,7 @@ fn sketch() -> error::Result<()> { while glfw_ctx.poll_events() { graphics_begin_draw(graphics)?; + graphics_light(graphics, light)?; graphics_record_command( graphics, DrawCommand::BackgroundColor(bevy::color::Color::srgb(0.06, 0.06, 0.08)), diff --git a/examples/particles_oriented.rs b/examples/particles_oriented.rs index 0740d130..b00876b6 100644 --- a/examples/particles_oriented.rs +++ b/examples/particles_oriented.rs @@ -55,7 +55,7 @@ fn sketch() -> error::Result<()> { transform_set_position(graphics, Vec3::new(0.0, 6.0, 18.0))?; transform_look_at(graphics, Vec3::new(0.0, 0.0, 0.0))?; - let _light = + let light = light_create_directional(graphics, bevy::color::Color::srgb(0.9, 0.85, 0.8), 300.0)?; let cube = geometry_box(0.6, 0.6, 0.6)?; @@ -113,6 +113,7 @@ fn sketch() -> error::Result<()> { while glfw_ctx.poll_events() { graphics_begin_draw(graphics)?; + graphics_light(graphics, light)?; graphics_record_command( graphics, DrawCommand::BackgroundColor(bevy::color::Color::srgb(0.06, 0.06, 0.08)), diff --git a/examples/particles_scatter.rs b/examples/particles_scatter.rs index 86afa512..260708e5 100644 --- a/examples/particles_scatter.rs +++ b/examples/particles_scatter.rs @@ -50,7 +50,7 @@ fn sketch() -> error::Result<()> { transform_set_position(graphics, Vec3::new(0.0, 0.4, 4.5))?; transform_look_at(graphics, Vec3::ZERO)?; - let _key = + let key = light_create_directional(graphics, bevy::color::Color::srgb(1.0, 0.95, 0.85), 4500.0)?; let source = geometry_sphere(1.2, 96, 48)?; @@ -86,6 +86,7 @@ fn sketch() -> error::Result<()> { while glfw_ctx.poll_events() { graphics_begin_draw(graphics)?; + graphics_light(graphics, key)?; graphics_record_command( graphics, DrawCommand::BackgroundColor(bevy::color::Color::srgb(0.03, 0.03, 0.05)), diff --git a/examples/particles_stress.rs b/examples/particles_stress.rs index ee91d64a..69a6917a 100644 --- a/examples/particles_stress.rs +++ b/examples/particles_stress.rs @@ -120,6 +120,9 @@ fn sketch() -> error::Result<()> { while glfw_ctx.poll_events() { graphics_begin_draw(graphics)?; + graphics_light(graphics, red)?; + graphics_light(graphics, green)?; + graphics_light(graphics, blue)?; graphics_record_command( graphics, DrawCommand::BackgroundColor(bevy::color::Color::srgb(0.04, 0.04, 0.07)), diff --git a/examples/pbr.rs b/examples/pbr.rs index d02a1843..4665f83e 100644 --- a/examples/pbr.rs +++ b/examples/pbr.rs @@ -48,6 +48,8 @@ fn sketch() -> error::Result<()> { while glfw_ctx.poll_events() { graphics_begin_draw(graphics)?; + graphics_light(graphics, dir_light)?; + graphics_light(graphics, point_light)?; graphics_record_command( graphics, diff --git a/examples/primitives_3d.rs b/examples/primitives_3d.rs index 4838c7db..330797b1 100644 --- a/examples/primitives_3d.rs +++ b/examples/primitives_3d.rs @@ -20,7 +20,7 @@ fn sketch() -> error::Result<()> { transform_set_position(graphics, Vec3::new(0.0, 80.0, 800.0))?; transform_look_at(graphics, Vec3::new(0.0, 0.0, 0.0))?; - let _light = + let light = light_create_directional(graphics, bevy::color::Color::srgb(0.9, 0.85, 0.8), 300.0)?; let pbr = material_create_pbr()?; @@ -30,6 +30,7 @@ fn sketch() -> error::Result<()> { while glfw_ctx.poll_events() { graphics_begin_draw(graphics)?; + graphics_light(graphics, light)?; graphics_record_command( graphics, diff --git a/examples/stroke_3d.rs b/examples/stroke_3d.rs index 5853686c..1f243f65 100644 --- a/examples/stroke_3d.rs +++ b/examples/stroke_3d.rs @@ -20,13 +20,14 @@ fn sketch() -> error::Result<()> { transform_set_position(graphics, Vec3::new(200.0, 150.0, 350.0))?; transform_look_at(graphics, Vec3::new(0.0, 0.0, 0.0))?; - let _light = + let light = light_create_directional(graphics, bevy::color::Color::srgb(0.9, 0.85, 0.8), 800.0)?; let mut angle: f32 = 0.0; while glfw_ctx.poll_events() { graphics_begin_draw(graphics)?; + graphics_light(graphics, light)?; graphics_record_command( graphics, diff --git a/examples/text_3d.rs b/examples/text_3d.rs index 95f2fd64..ada1c6cd 100644 --- a/examples/text_3d.rs +++ b/examples/text_3d.rs @@ -48,6 +48,7 @@ fn sketch() -> error::Result<()> { while glfw_ctx.poll_events() { graphics_begin_draw(graphics)?; + graphics_light(graphics, dir_light)?; graphics_record_command( graphics, diff --git a/src/prelude.rs b/src/prelude.rs index 34f95a3d..cf6287f8 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -11,8 +11,8 @@ pub use processing_midi::{ }; pub use processing_render::{ render::command::{ - ArcMode, BlendMode, DrawCommand, ShapeKind, ShapeMode, StrokeCapMode, StrokeJoinMode, - TextAlignH, TextAlignV, TextStyle, TextWrapMode, custom_blend_state, + ArcMode, BlendMode, DrawCommand, LightKind, ShapeKind, ShapeMode, StrokeCapMode, + StrokeJoinMode, TextAlignH, TextAlignV, TextStyle, TextWrapMode, custom_blend_state, }, render::filter::Filter, *,