Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file removed .DS_Store
Binary file not shown.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules/
.env
16 changes: 12 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ An interactive web app built with threejs, mediapipe computer vision, web speech

- Say "drag", "rotate", "scale", or "animate" to change the interaction mode
- Pinch fingers to control the 3D model
- Drag/drop a new 3D model onto the page to import it (GLTF format only for now)
- Drag/drop or import a new 3D model (.gltf or .glb) onto the page to import it

[Video](https://x.com/measure_plan/status/1929900748235550912) | [Live Demo](https://collidingscopes.github.io/3d-model-playground/)

Expand All @@ -32,11 +32,19 @@ git clone https://github.com/collidingScopes/3d-model-playground
# Navigate to the project directory
cd 3d-model-playground

# Serve with your preferred method (example using Python)
# Install dependencies
npm install

# Start the dev server (uses vite)
npm run dev

# Or serve with your preferred method (example using Python)
python -m http.server
```

Then navigate to `http://localhost:8000` in your browser.
Then navigate to `http://localhost:8000` or the port printed by `npm run dev` in your browser.

Vite handles the dev/preview server, so any scripts such as `npm run dev` or `npm run preview` rely on it.

## License

Expand Down Expand Up @@ -71,4 +79,4 @@ If you found this tool useful, feel free to buy me a coffee.

My name is Alan, and I enjoy building open source software for computer vision, games, and more. This would be much appreciated during late-night coding sessions!

[![Buy Me A Coffee](https://www.buymeacoffee.com/assets/img/custom_images/yellow_img.png)](https://www.buymeacoffee.com/stereoDrift)
[![Buy Me A Coffee](https://www.buymeacoffee.com/assets/img/custom_images/yellow_img.png)](https://www.buymeacoffee.com/stereoDrift)
2 changes: 1 addition & 1 deletion SpeechManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -358,4 +358,4 @@ export var SpeechManager = /*#__PURE__*/ function() {
}
]);
return SpeechManager;
}();
}();
67 changes: 64 additions & 3 deletions game.js
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,9 @@ export var Game = /*#__PURE__*/ function() {
this.speechManager = null;
this.speechBubble = null;
this.speechBubbleTimeout = null;
this.openAIApiKey = window.OPENAI_API_KEY || null;
this.importInput = null;
this.importButton = null;
this.isSpeechActive = false; // Track if speech recognition is active for styling
this.grabbingHandIndex = -1; // -1: no hand, 0: first hand, 1: second hand grabbing
this.pickedUpModel = null; // Reference to the model being dragged
Expand Down Expand Up @@ -395,6 +398,36 @@ export var Game = /*#__PURE__*/ function() {
this.speechBubble.style.pointerEvents = 'none'; // Not interactive
this.speechBubble.innerHTML = "..."; // Default text
this.renderDiv.appendChild(this.speechBubble);
// Import button and file input
this.importInput = document.createElement("input");
this.importInput.type = "file";
this.importInput.accept = ".gltf,.glb,model/gltf+json,model/gltf-binary";
this.importInput.style.display = "none";
this.importInput.addEventListener("change", function(event) {
if (event.target.files && event.target.files[0]) {
_this._loadDroppedModel(event.target.files[0]);
event.target.value = "";
}
});
this.importButton = document.createElement("button");
this.importButton.innerText = "Import Model";
this.importButton.style.position = "absolute";
this.importButton.style.top = "60px";
this.importButton.style.left = "50%";
this.importButton.style.transform = "translateX(-50%)";
this.importButton.style.zIndex = "30";
this.importButton.style.padding = "12px 24px";
this.importButton.style.fontSize = "20px";
this.importButton.style.border = "2px solid black";
this.importButton.style.borderRadius = "4px";
this.importButton.style.cursor = "pointer";
this.importButton.style.background = "white";
this.importButton.style.boxShadow = "2px 2px 0px black";
this.importButton.addEventListener("click", function() {
return _this.importInput.click();
});
this.renderDiv.appendChild(this.importButton);
this.renderDiv.appendChild(this.importInput);
// Animation buttons container
this.animationButtonsContainer = document.createElement('div');
this.animationButtonsContainer.id = 'animation-buttons-container';
Expand Down Expand Up @@ -432,8 +465,8 @@ export var Game = /*#__PURE__*/ function() {
var button = document.createElement('button');
button.innerText = mode;
button.id = "interaction-mode-".concat(mode.toLowerCase());
button.style.padding = '10px 22px'; // Increased padding
button.style.fontSize = '18px'; // Increased font size further
button.style.padding = '16px 28px'; // Increased padding
button.style.fontSize = 'clamp(22px, 4vw, 32px)'; // Increased font size further
button.style.border = '2px solid black'; // Consistent black border
button.style.borderRadius = '4px'; // Sharper corners
button.style.cursor = 'pointer';
Expand Down Expand Up @@ -1517,6 +1550,9 @@ export var Game = /*#__PURE__*/ function() {
if (finalTranscript) {
_this.speechBubble.innerHTML = finalTranscript;
_this.speechBubble.style.opacity = '1';
_this._sendAIMessage(finalTranscript).then(function(reply){
_this.speechBubble.innerHTML = reply;
}).catch(function(err){ console.error("AI error", err); });
_this.speechBubbleTimeout = setTimeout(function() {
_this.speechBubble.innerHTML = "...";
_this.speechBubble.style.opacity = '0.7';
Expand Down Expand Up @@ -1598,6 +1634,31 @@ export var Game = /*#__PURE__*/ function() {
}
}
},
{
key: "_sendAIMessage",
value: function _sendAIMessage(message) {
if (!this.openAIApiKey) {
return Promise.resolve("(AI API key not set)");
}
return fetch("https://api.openai.com/v1/chat/completions", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer " + this.openAIApiKey
},
body: JSON.stringify({
model: "gpt-3.5-turbo",
messages: [{ role: "user", content: message }]
})
}).then(function(res){ return res.json(); }).then(function(data){
if (data && data.choices && data.choices[0] && data.choices[0].message) {
return data.choices[0].message.content.trim();
} else {
throw new Error("Invalid AI response");
}
});
}
},
{
key: "_setInteractionMode",
value: function _setInteractionMode(mode) {
Expand Down Expand Up @@ -1889,4 +1950,4 @@ export var Game = /*#__PURE__*/ function() {
}
]);
return Game;
}();
}();
14 changes: 1 addition & 13 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,11 @@
<meta property="twitter:description" content="Control 3D models with hand gestures & voice commands">
<meta property="twitter:image" content="https://raw.githubusercontent.com/collidingScopes/3d-model-playground/main/assets/siteOGImage.jpg">

<script defer src="https://cloud.umami.is/script.js" data-website-id="eb59c81c-27cb-4e1d-9e8c-bfbe70c48cd9"></script>
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/[email protected]/build/three.module.js",
"three/examples/": "https://unpkg.com/[email protected]/examples/jsm/",
"three/addons/": "https://unpkg.com/[email protected]/examples/jsm/",
"three/loaders/": "https://unpkg.com/[email protected]/examples/jsm/loaders/"
}
}
Expand All @@ -38,18 +37,7 @@
<body style="width: 100%; height: 100%; overflow: hidden; margin: 0;">
<div id="renderDiv" style="width: 100%; height: 100%; margin: 0;">
<div id="instruction-text" class="text-box"></div>
<div id="video-link" class="text-box">
<a href="https://x.com/measure_plan/status/1929900748235550912" target="_blank">Video Demo</a>
</div>
<div id="social-links" class="text-box">
<a href="https://www.x.com/measure_plan/" target="_blank">Twitter</a><br>
<a href="https://www.instagram.com/stereo.drift/" target="_blank">Instagram</a>
</div>
<div id="coffee-link" class="text-box">
<a href="https://buymeacoffee.com/stereodrift" target="_blank">Buy me a coffee 💛</a>
</div>
</div>
<script type="module" src="main.js"></script>

</body>
</html>
8 changes: 4 additions & 4 deletions main.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { Game } from './game.js';

// Get the render target div
var renderDiv = document.getElementById('renderDiv');
const renderDiv = document.getElementById('renderDiv');

// Check if renderDiv exists
if (!renderDiv) {
console.error('Fatal Error: renderDiv element not found.');
} else {
// Initialize the game with the render target
var game = new Game(renderDiv);
// Start the game
game.start(); // The actual setup happens async within the Game class constructor
const game = new Game(renderDiv);
}
Loading