From c93e7c1998ab277ca2fcdb6604654ae05755cfe2 Mon Sep 17 00:00:00 2001 From: Banyapon Poolsawas Date: Tue, 6 Aug 2024 10:22:14 +0700 Subject: [PATCH 1/2] First Commit with all script Export GLB, favcion, vr controller --- aframe_exporter/LICENSE | 23 ++++++++++++ aframe_exporter/README.md | 52 +++++++++++++++++++++++++++ aframe_exporter/__init__.py | 27 +++++++++++++++ aframe_exporter/aframe_exporter.py | 54 +++++++++++++++++++++++++++++ aframe_exporter/icons/favicon.ico | Bin 0 -> 15406 bytes 5 files changed, 156 insertions(+) create mode 100644 aframe_exporter/LICENSE create mode 100644 aframe_exporter/README.md create mode 100644 aframe_exporter/__init__.py create mode 100644 aframe_exporter/aframe_exporter.py create mode 100644 aframe_exporter/icons/favicon.ico diff --git a/aframe_exporter/LICENSE b/aframe_exporter/LICENSE new file mode 100644 index 000000000..3d0f5908e --- /dev/null +++ b/aframe_exporter/LICENSE @@ -0,0 +1,23 @@ +MIT License + +Copyright (c) 2024 Banyapon Poolsawas associated with +College of Creative Design and Entertainment Technology, +Dhurakij Pundit University and Daydev Co., Ltd. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/aframe_exporter/README.md b/aframe_exporter/README.md new file mode 100644 index 000000000..f417eca2a --- /dev/null +++ b/aframe_exporter/README.md @@ -0,0 +1,52 @@ +# Blender A-Frame Exporter + +[![Blender Version](https://img.shields.io/badge/Blender-3.0+-orange.svg)](https://www.blender.org/) [![Blender Version](https://img.shields.io/badge/Blender-4.0+-orange.svg)](https://www.blender.org/) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) + +This Blender add-on simplifies the process of exporting your 3D scenes into interactive web experiences using A-Frame, a popular web framework for building virtual reality (VR) and augmented reality (AR) experiences. + +## Features + +![Showcase](https://github.com/banyapon/Blender-Aframe-Exporter/blob/main/dist/8.png?raw=true) + +* **Seamless Export:** Quickly export your entire Blender scene, including models, materials, and animations, to A-Frame HTML and GLB (binary glTF) files. +* **Collision Detection:** Automatically add collision detection to your exported models using A-Frame's physics system, enabling interactive experiences. +* **Camera Controls:** Easily set up basic WASD controls for navigating your A-Frame scene with the camera. +* **Customization:** Customize the generated A-Frame code to add additional features, components, or interactions. + +## Installation + +1. Download the latest release of the add-on (`.zip` file) from the [Releases](https://github.com/banyapon/Blender-Aframe-Exporter/releases/tag/release) page. +2. In Blender, go to `Edit` > `Preferences` > `Add-ons` > `Install...` +![Go to `Edit` > `Preferences` > `Add-ons` > `Install](https://raw.githubusercontent.com/banyapon/Blender-Aframe-Exporter/main/dist/1.png) +3. Select the downloaded `.zip` file and click `Install Add-on`. +![Zip File](https://github.com/banyapon/Blender-Aframe-Exporter/blob/main/dist/2.png?raw=true) +4. Enable the "A-Frame Exporter" add-on in the list. +![Enable add-on](https://github.com/banyapon/Blender-Aframe-Exporter/blob/main/dist/3.png?raw=true) + +## Usage + +1. Create your 3D scene in Blender. +![Create Scene](https://github.com/banyapon/Blender-Aframe-Exporter/blob/main/dist/5.png?raw=true) +2. Go to `File` > `Export` > `A-Frame (.html)`. + + ![Export Aframe](https://github.com/banyapon/Blender-Aframe-Exporter/blob/main/dist/6.png?raw=true) + +3. Choose a location to save the files and click `Export A-Frame`. +![VS Code](https://github.com/banyapon/Blender-Aframe-Exporter/blob/main/dist/7.png?raw=true) +4. The add-on will generate an HTML file (`your_scene_name.html`) and a GLB file (`your_scene_name.glb`). +5. Open the HTML file in a web browser to view your interactive A-Frame scene. +![WebXR](https://github.com/banyapon/Blender-Aframe-Exporter/blob/main/dist/9.png?raw=true) + +## Additional Notes + +* Make sure you have a web server running to view the exported HTML file correctly. +* For more advanced A-Frame features and customization, refer to the [A-Frame documentation](https://aframe.io/docs/). + +## Contributing + +Contributions are welcome! Feel free to submit issues, feature requests, or pull requests. + +## License + +This add-on is released under the MIT License. diff --git a/aframe_exporter/__init__.py b/aframe_exporter/__init__.py new file mode 100644 index 000000000..d7c0f08d2 --- /dev/null +++ b/aframe_exporter/__init__.py @@ -0,0 +1,27 @@ +import bpy +from . import aframe_exporter + +bl_info = { + "name": "A-Frame Exporter", + "author": "Banyapon Poolsawas", + "version": (1, 0, 2), + "blender": (4, 0, 1), + "location": "File > Export", + "description": "Export Blender scene to A-Frame HTML and GLB for WebXR", + "warning": "", + "category": "Export", +} + +def menu_func_export(self, context): + self.layout.operator(aframe_exporter.ExportAFrame.bl_idname, text="A-Frame (.html)") + +def register(): + bpy.utils.register_class(aframe_exporter.ExportAFrame) + bpy.types.TOPBAR_MT_file_export.append(menu_func_export) + +def unregister(): + bpy.utils.unregister_class(aframe_exporter.ExportAFrame) + bpy.types.TOPBAR_MT_file_export.remove(menu_func_export) + +if __name__ == "__main__": + register() \ No newline at end of file diff --git a/aframe_exporter/aframe_exporter.py b/aframe_exporter/aframe_exporter.py new file mode 100644 index 000000000..8e6f19ebc --- /dev/null +++ b/aframe_exporter/aframe_exporter.py @@ -0,0 +1,54 @@ +import bpy +import os +import shutil +from bpy_extras.io_utils import ExportHelper + +class ExportAFrame(bpy.types.Operator, ExportHelper): + bl_idname = "export_scene.aframe" + bl_label = "Export A-Frame" + filename_ext = ".html" + + def execute(self, context): + filepath = self.filepath + glb_filepath = os.path.splitext(filepath)[0] + ".glb" + + # Export GLB + bpy.ops.export_scene.gltf(filepath=glb_filepath, export_format='GLB') + + # Copy favicon.ico if it exists + favicon_path = os.path.join(os.path.dirname(__file__), "icons", "favicon.ico") + if os.path.exists(favicon_path): + shutil.copy(favicon_path, os.path.dirname(filepath)) + + # Create HTML (A-Frame) + with open(filepath, "w") as f: + f.write(f""" + + + + + + Aframe Exporter + + + + + + + + + + + + + + + + + + + + + """) + + return {'FINISHED'} diff --git a/aframe_exporter/icons/favicon.ico b/aframe_exporter/icons/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..1a2d02c47b276490488702e8068f3ab7ee4ba3ea GIT binary patch literal 15406 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 Date: Sat, 17 Aug 2024 03:23:21 +0200 Subject: [PATCH 2/2] remove wasd-control add aframe-extras-min.js and create local server function on export properties --- aframe_exporter/aframe_exporter.py | 103 +++++++++++++++++++++++++++-- 1 file changed, 96 insertions(+), 7 deletions(-) diff --git a/aframe_exporter/aframe_exporter.py b/aframe_exporter/aframe_exporter.py index 8e6f19ebc..e2ec2fb67 100644 --- a/aframe_exporter/aframe_exporter.py +++ b/aframe_exporter/aframe_exporter.py @@ -1,13 +1,58 @@ import bpy import os import shutil +import http.server +import socketserver +import threading +import webbrowser +from bpy.props import BoolProperty, IntProperty from bpy_extras.io_utils import ExportHelper +class ServerThread(threading.Thread): + def __init__(self, directory, port): + super().__init__() + self.directory = directory + self.port = port + self.httpd = None + self.error = None + + def run(self): + os.chdir(self.directory) + + # Error handling for the server + try: + handler = http.server.SimpleHTTPRequestHandler + with socketserver.TCPServer(("", self.port), handler) as httpd: + self.httpd = httpd + print(f"Serving at port {self.port}") + httpd.serve_forever() + except OSError as e: + self.error = e + + def stop(self): + if self.httpd: + self.httpd.shutdown() + class ExportAFrame(bpy.types.Operator, ExportHelper): bl_idname = "export_scene.aframe" bl_label = "Export A-Frame" filename_ext = ".html" + start_server: BoolProperty( + name="Start Local Server", + description="Automatically start a local server after export", + default=False + ) + + port: IntProperty( + name="Port", + description="Port number for the local server", + default=8200, + min=1, + max=65535 + ) + + def execute(self, context): filepath = self.filepath glb_filepath = os.path.splitext(filepath)[0] + ".glb" @@ -15,21 +60,21 @@ class ExportAFrame(bpy.types.Operator, ExportHelper): # Export GLB bpy.ops.export_scene.gltf(filepath=glb_filepath, export_format='GLB') - # Copy favicon.ico if it exists - favicon_path = os.path.join(os.path.dirname(__file__), "icons", "favicon.ico") + # Copy favicon + favicon_path = os.path.join(os.path.dirname(__file__), "icons", "favicon.ico") if os.path.exists(favicon_path): - shutil.copy(favicon_path, os.path.dirname(filepath)) + shutil.copy(favicon_path, os.path.dirname(filepath)) # Create HTML (A-Frame) with open(filepath, "w") as f: f.write(f""" - - + Aframe Exporter + @@ -39,7 +84,7 @@ class ExportAFrame(bpy.types.Operator, ExportHelper): - + @@ -47,8 +92,52 @@ class ExportAFrame(bpy.types.Operator, ExportHelper): - """) + if self.start_server: + self.server_thread = ServerThread(os.path.dirname(filepath), self.port) + self.server_thread.start() + + if self.server_thread.error: # Check for errors + self.report({'ERROR'}, f"Failed to start server: {self.server_thread.error}") + return {'CANCELLED'} + + url = f"http://localhost:{self.port}/{os.path.basename(filepath)}" + else: + url = "file://" + filepath + + webbrowser.open_new_tab(url) return {'FINISHED'} + + def start_local_server(self, directory, port): + os.chdir(directory) + handler = http.server.SimpleHTTPRequestHandler + with socketserver.TCPServer(("", port), handler) as httpd: + print(f"Serving at port {port}") + threading.Thread(target=httpd.serve_forever).start() + + def invoke(self, context, event): + context.window_manager.fileselect_add(self) + return {'RUNNING_MODAL'} + +class ExportAFramePanel(bpy.types.Panel): + bl_label = "A-Frame Export Settings" # Label for the panel + bl_idname = "OBJECT_PT_aframe_export" # Unique identifier for the panel + bl_space_type = 'PROPERTIES' # Panel location (Properties Editor) + bl_region_type = 'WINDOW' # Panel type (window) + bl_context = "scene" # Context for the panel (Scene) + bl_options = {'DEFAULT_CLOSED'} # Panel is closed by default + + def draw(self, context): + layout = self.layout + + # Export Button + layout.operator("export_scene.aframe", text="Export A-Frame") # Add the export operator button + + # Local Server Options + row = layout.row() # Create a new row for the checkbox and button + row.prop(context.scene, "aframe_local_server", text="Start Local Server") # Checkbox to toggle local server + if context.scene.aframe_local_server: # Show port input if the checkbox is checked + row = layout.row() # Create a new row for the port input + row.prop(context.scene, "aframe_server_port", text="Port") # Input field for the port number \ No newline at end of file -- 2.30.2