Last active
July 19, 2025 22:40
-
-
Save celsowm/35fd29907f72088ef1972a2b7da3a021 to your computer and use it in GitHub Desktop.
Jo Engine 3D editor and generator code tool
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="pt-br"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Editor de Cenário 3D Avançado para Jo Engine</title> | |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/atom-one-dark.min.css"> | |
<style> | |
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; margin: 0; background-color: #333; color: #eee; overflow: hidden; } | |
#main-container { display: flex; width: 100vw; height: 100vh; } | |
#viewport { flex-grow: 1; height: 100%; } | |
#controls { width: 350px; background-color: #282c34; padding: 15px; box-sizing: border-box; overflow-y: auto; height: 100%; display: flex; flex-direction: column; } | |
.control-group { margin-bottom: 20px; border-bottom: 1px solid #444; padding-bottom: 15px; } | |
h2, h3 { margin-top: 0; color: #61afef; border-bottom: 1px solid #444; padding-bottom: 5px; } | |
label { display: block; margin-bottom: 5px; font-size: 0.9em; color: #abb2bf; } | |
input[type="number"], input[type="text"], input[type="color"], input[type="file"], textarea { width: 95%; background-color: #21252b; border: 1px solid #3b4048; color: #eee; padding: 8px; border-radius: 4px; margin-bottom: 10px; font-family: "Courier New", Courier, monospace; } | |
input[type="file"] { padding: 4px; } | |
textarea { resize: vertical; } | |
.vector-input { display: flex; justify-content: space-between; } | |
.vector-input div { width: 32%; } | |
.vector-input input { width: 85%; } | |
button { width: 100%; padding: 10px; background-color: #61afef; border: none; color: #282c34; font-weight: bold; border-radius: 4px; cursor: pointer; margin-top: 10px; } | |
button:hover { background-color: #7cc2ff; } | |
#generate-zip-btn { background-color: #98c379; } | |
#generate-zip-btn:hover { background-color: #aee08f; } | |
#update-mesh-btn { background-color: #e5c07b; } | |
#update-mesh-btn:hover { background-color: #f0d099; } | |
#object-properties, #vdp2-nbg0-plane-preview, #vdp2-nbg1-controls, #code-container, #mesh-properties { display: none; } | |
#code-container { position: fixed; bottom: 0; left: 0; width: 100%; height: 50%; background-color: rgba(20, 20, 20, 0.95); border-top: 2px solid #61afef; flex-direction: column; z-index: 100; } | |
#code-header { background-color: #282c34; padding: 5px 15px; display: flex; justify-content: space-between; align-items: center; } | |
#close-code-btn { background: #e06c75; color: #fff; border: none; padding: 5px 10px; cursor: pointer; border-radius: 4px; width: auto; margin: 0; } | |
#generated-code-wrapper { font-family: "Courier New", Courier, monospace; white-space: pre; padding: 0; overflow: auto; flex-grow: 1; font-size: 14px; background-color: #282c34; } | |
#generated-code-wrapper code { padding: 15px; } | |
</style> | |
</head> | |
<body> | |
<div id="main-container"> | |
<div id="viewport"></div> | |
<div id="controls"> | |
<div class="control-group"> | |
<h2>Cena 3D</h2> | |
<button id="add-cube">Adicionar Cubo</button> | |
<button id="add-plane">Adicionar Plano</button> | |
<button id="add-mesh">Adicionar Malha Customizada</button> | |
</div> | |
<div id="object-properties" class="control-group"> | |
<h3>Objeto Selecionado</h3> | |
<label for="obj-name">Nome:</label> <input type="text" id="obj-name"> | |
<label>Posição (X, Y, Z):</label> | |
<div class="vector-input"> | |
<div><input type="number" id="pos-x" step="1"></div> <div><input type="number" id="pos-y" step="1"></div> <div><input type="number" id="pos-z" step="1"></div> | |
</div> | |
<label>Rotação (X, Y, Z graus):</label> | |
<div class="vector-input"> | |
<div><input type="number" id="rot-x" step="1"></div> <div><input type="number" id="rot-y" step="1"></div> <div><input type="number" id="rot-z" step="1"></div> | |
</div> | |
<label>Escala (X, Y, Z):</label> | |
<div class="vector-input"> | |
<div><input type="number" id="scale-x" step="0.1"></div> <div><input type="number" id="scale-y" step="0.1"></div> <div><input type="number" id="scale-z" step="0.1"></div> | |
</div> | |
<div id="material-properties"> | |
<label for="obj-color">Cor:</label> <input type="color" id="obj-color" value="#ffffff"> | |
<label for="obj-texture">Textura (PNG/JPG):</label> <input type="file" id="obj-texture" accept="image/png, image/jpeg"> | |
</div> | |
</div> | |
<div id="mesh-properties" class="control-group"> | |
<h3>Geometria da Malha (JSON)</h3> | |
<textarea id="mesh-definition" rows="15"></textarea> | |
<button id="update-mesh-btn">Atualizar Malha</button> | |
</div> | |
<div class="control-group"> | |
<h3>VDP2 / Fundo</h3> | |
<label for="vdp2-bg-color">Cor de Fundo Global</label> | |
<input type="color" id="vdp2-bg-color" value="#505050"> | |
<label for="vdp2-nbg1-texture">Fundo Rolante (NBG1)</label> | |
<input type="file" id="vdp2-nbg1-texture" accept="image/png, image/jpeg"> | |
<div id="vdp2-nbg1-controls"> | |
<label>Velocidade de Scroll (X, Y):</label> | |
<div class="vector-input"> | |
<div><input type="number" id="vdp2-nbg1-scroll-x" step="0.1" value="0"></div> | |
<div><input type="number" id="vdp2-nbg1-scroll-y" step="0.1" value="0"></div> | |
</div> | |
</div> | |
<label for="vdp2-nbg0-texture">Plano Infinito (NBG0)</label> | |
<input type="file" id="vdp2-nbg0-texture" accept="image/png, image/jpeg"> | |
<div id="vdp2-nbg0-plane-preview"> | |
<label>Posição Y:</label> <input type="number" id="vdp2-nbg0-pos-y" step="1" value="-50"> | |
<label>Escala da Textura:</label> <input type="number" id="vdp2-nbg0-scale" step="0.1" value="1.0"> | |
</div> | |
</div> | |
<div style="margin-top: auto;"> | |
<button id="show-code-btn">Visualizar Código C</button> | |
<button id="generate-zip-btn">Gerar Projeto (.zip)</button> | |
</div> | |
</div> | |
</div> | |
<div id="code-container"> | |
<div id="code-header"> | |
<h3>Código Gerado para Jo Engine</h3> | |
<button id="close-code-btn">Fechar</button> | |
</div> | |
<pre id="generated-code-wrapper"><code id="generated-code" class="language-c"></code></pre> | |
</div> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script> | |
<script type="importmap"> | |
{ "imports": { "three": "https://unpkg.com/[email protected]/build/three.module.js", "three/addons/": "https://unpkg.com/[email protected]/examples/jsm/" } } | |
</script> | |
<script type="module"> | |
import * as THREE from 'three'; | |
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; | |
import { TransformControls } from 'three/addons/controls/TransformControls.js'; | |
let scene, camera, renderer, orbitControls, transformControls; | |
let selectedObject = null; | |
const objects = []; | |
let cubeCounter = 1, planeCounter = 1, meshCounter = 1; | |
let vdp2_infinite_plane = null; | |
const vdp2_settings = { | |
bgColor: '#505050', | |
nbg1: { file: null, scrollX: 0, scrollY: 0, texture: null }, | |
nbg0: { file: null, posY: -50, scale: 1.0, texture: null } | |
}; | |
const viewport = document.getElementById('viewport'); | |
const objectPropertiesPanel = document.getElementById('object-properties'); | |
const meshPropertiesPanel = document.getElementById('mesh-properties'); | |
const ui = { | |
name: document.getElementById('obj-name'), | |
posX: document.getElementById('pos-x'), posY: document.getElementById('pos-y'), posZ: document.getElementById('pos-z'), | |
rotX: document.getElementById('rot-x'), rotY: document.getElementById('rot-y'), rotZ: document.getElementById('rot-z'), | |
scaleX: document.getElementById('scale-x'), scaleY: document.getElementById('scale-y'), scaleZ: document.getElementById('scale-z'), | |
color: document.getElementById('obj-color'), texture: document.getElementById('obj-texture'), | |
materialProperties: document.getElementById('material-properties'), | |
meshDefinition: document.getElementById('mesh-definition'), | |
vdp2BgColor: document.getElementById('vdp2-bg-color'), | |
vdp2Nbg1Texture: document.getElementById('vdp2-nbg1-texture'), | |
vdp2Nbg1Controls: document.getElementById('vdp2-nbg1-controls'), | |
vdp2Nbg1ScrollX: document.getElementById('vdp2-nbg1-scroll-x'), | |
vdp2Nbg1ScrollY: document.getElementById('vdp2-nbg1-scroll-y'), | |
vdp2Nbg0Texture: document.getElementById('vdp2-nbg0-texture'), | |
vdp2Nbg0PlanePreview: document.getElementById('vdp2-nbg0-plane-preview'), | |
vdp2Nbg0PosY: document.getElementById('vdp2-nbg0-pos-y'), | |
vdp2Nbg0Scale: document.getElementById('vdp2-nbg0-scale'), | |
codeContainer: document.getElementById('code-container'), | |
generatedCodeEl: document.getElementById('generated-code') | |
}; | |
const defaultMeshDefinition = `[ | |
{ | |
"color": "#FFFF00", | |
"vertices": [ | |
{ "x": -50, "y": -50, "z": 0 }, | |
{ "x": 50, "y": -50, "z": 0 }, | |
{ "x": 50, "y": 50, "z": 0 }, | |
{ "x": -50, "y": 50, "z": 0 } | |
] | |
}, | |
{ | |
"color": "#00FFFF", | |
"vertices": [ | |
{ "x": -50, "y": -50, "z": -50 }, | |
{ "x": -50, "y": 50, "z": -50 }, | |
{ "x": -50, "y": 50, "z": 50 }, | |
{ "x": -50, "y": -50, "z": 50 } | |
] | |
} | |
]`; | |
function init() { | |
scene = new THREE.Scene(); | |
scene.background = new THREE.Color(vdp2_settings.bgColor); | |
camera = new THREE.PerspectiveCamera(75, viewport.clientWidth / viewport.clientHeight, 0.1, 2000); | |
camera.position.set(50, 60, 100); | |
renderer = new THREE.WebGLRenderer({ antialias: true }); | |
renderer.setSize(viewport.clientWidth, viewport.clientHeight); | |
renderer.setPixelRatio(window.devicePixelRatio); | |
viewport.appendChild(renderer.domElement); | |
const gridHelper = new THREE.GridHelper(500, 20); | |
scene.add(gridHelper); | |
const ambientLight = new THREE.AmbientLight(0xffffff, 0.7); | |
scene.add(ambientLight); | |
const directionalLight = new THREE.DirectionalLight(0xffffff, 1); | |
directionalLight.position.set(5, 10, 7); | |
scene.add(directionalLight); | |
orbitControls = new OrbitControls(camera, renderer.domElement); | |
transformControls = new TransformControls(camera, renderer.domElement); | |
scene.add(transformControls); | |
transformControls.addEventListener('dragging-changed', e => { orbitControls.enabled = !e.value; }); | |
transformControls.addEventListener('objectChange', () => updatePropertiesPanel(selectedObject)); | |
document.getElementById('add-cube').addEventListener('click', addCube); | |
document.getElementById('add-plane').addEventListener('click', addPlane); | |
document.getElementById('add-mesh').addEventListener('click', addCustomMesh); | |
document.getElementById('update-mesh-btn').addEventListener('click', () => { if(selectedObject && selectedObject.userData.type === 'custom_mesh') { selectedObject.userData.definition = ui.meshDefinition.value; rebuildCustomMeshFromData(selectedObject); } }); | |
document.getElementById('show-code-btn').addEventListener('click', showGeneratedCode); | |
document.getElementById('generate-zip-btn').addEventListener('click', generateProjectZip); | |
document.getElementById('close-code-btn').addEventListener('click', () => ui.codeContainer.style.display = 'none'); | |
renderer.domElement.addEventListener('click', onMouseClick); | |
window.addEventListener('resize', onWindowResize); | |
window.addEventListener('keydown', onKeyDown); | |
setupInputListeners(); | |
animate(); | |
} | |
function onWindowResize() { camera.aspect = viewport.clientWidth / viewport.clientHeight; camera.updateProjectionMatrix(); renderer.setSize(viewport.clientWidth, viewport.clientHeight); } | |
function onKeyDown(event) { if (document.activeElement.tagName === 'INPUT' || document.activeElement.tagName === 'TEXTAREA') return; switch (event.key) { case 'w': transformControls.setMode('translate'); break; case 'e': transformControls.setMode('rotate'); break; case 'r': transformControls.setMode('scale'); break; case 'Delete': case 'Backspace': if (selectedObject) removeObject(selectedObject); break; } } | |
function onMouseClick(event) { | |
const rect = renderer.domElement.getBoundingClientRect(); | |
const mouse = { x: ((event.clientX - rect.left) / rect.width) * 2 - 1, y: -((event.clientY - rect.top) / rect.height) * 2 + 1 }; | |
const raycaster = new THREE.Raycaster(); | |
raycaster.setFromCamera(mouse, camera); | |
const intersects = raycaster.intersectObjects(objects, true); | |
if (intersects.length > 0) { | |
let objectToSelect = intersects[0].object; | |
while (objectToSelect.parent && !objectToSelect.userData.isSelectable) { | |
objectToSelect = objectToSelect.parent; | |
} | |
selectObject(objectToSelect); | |
} else { | |
selectObject(null); | |
} | |
} | |
function addCube() { const geometry = new THREE.BoxGeometry(50, 50, 50); const material = new THREE.MeshStandardMaterial({ color: 0xffffff }); const cube = new THREE.Mesh(geometry, material); cube.name = `caixa_${cubeCounter++}`; cube.userData = { type: 'cube', baseSize: 50, textureFile: null, textureData: null }; addObjectToScene(cube); } | |
function addPlane() { const geometry = new THREE.PlaneGeometry(100, 100); const material = new THREE.MeshStandardMaterial({ color: 0xffffff, side: THREE.DoubleSide }); const plane = new THREE.Mesh(geometry, material); plane.name = `plano_${planeCounter++}`; plane.rotation.x = -Math.PI / 2; plane.userData = { type: 'plane', baseSize: 100, textureFile: null, textureData: null }; addObjectToScene(plane); } | |
function addCustomMesh() { const group = new THREE.Group(); group.name = `malha_${meshCounter++}`; group.userData = { definition: defaultMeshDefinition, type: 'custom_mesh' }; rebuildCustomMeshFromData(group); addObjectToScene(group); } | |
function addObjectToScene(obj) { obj.userData.type = obj.userData.type || 'unknown'; obj.userData.isSelectable = true; scene.add(obj); objects.push(obj); selectObject(obj); } | |
function removeObject(obj) { if (!obj) return; const index = objects.indexOf(obj); if (index > -1) objects.splice(index, 1); transformControls.detach(); if (obj.isGroup) { obj.children.forEach(child => { child.geometry.dispose(); child.material.dispose(); }); } else { obj.geometry.dispose(); obj.material.dispose(); } scene.remove(obj); selectObject(null); } | |
function selectObject(obj) { selectedObject = obj; if(obj) { transformControls.attach(obj); objectPropertiesPanel.style.display = 'block'; meshPropertiesPanel.style.display = obj.userData.type === 'custom_mesh' ? 'block' : 'none'; ui.materialProperties.style.display = obj.userData.type !== 'custom_mesh' ? 'block' : 'none'; updatePropertiesPanel(obj); } else { transformControls.detach(); objectPropertiesPanel.style.display = 'none'; meshPropertiesPanel.style.display = 'none'; } } | |
function updatePropertiesPanel(obj) { if(!obj) return; ui.name.value = obj.name; ui.posX.value = obj.position.x.toFixed(2); ui.posY.value = obj.position.y.toFixed(2); ui.posZ.value = obj.position.z.toFixed(2); ui.rotX.value = THREE.MathUtils.radToDeg(obj.rotation.x).toFixed(2); ui.rotY.value = THREE.MathUtils.radToDeg(obj.rotation.y).toFixed(2); ui.rotZ.value = THREE.MathUtils.radToDeg(obj.rotation.z).toFixed(2); ui.scaleX.value = obj.scale.x.toFixed(2); ui.scaleY.value = obj.scale.y.toFixed(2); ui.scaleZ.value = obj.scale.z.toFixed(2); if(obj.userData.type === 'custom_mesh') { ui.meshDefinition.value = obj.userData.definition; } else { ui.color.value = '#' + obj.material.color.getHexString(); ui.texture.value = ''; } } | |
function setupInputListeners() { | |
const updateSel = (fn) => (e) => { if (selectedObject) fn(e.target.value); }; | |
ui.name.addEventListener('change', updateSel(v => selectedObject.name = v)); ui.posX.addEventListener('change', updateSel(v => selectedObject.position.x = parseFloat(v))); ui.posY.addEventListener('change', updateSel(v => selectedObject.position.y = parseFloat(v))); ui.posZ.addEventListener('change', updateSel(v => selectedObject.position.z = parseFloat(v))); ui.rotX.addEventListener('change', updateSel(v => selectedObject.rotation.x = THREE.MathUtils.degToRad(v))); ui.rotY.addEventListener('change', updateSel(v => selectedObject.rotation.y = THREE.MathUtils.degToRad(v))); ui.rotZ.addEventListener('change', updateSel(v => selectedObject.rotation.z = THREE.MathUtils.degToRad(v))); ui.scaleX.addEventListener('change', updateSel(v => selectedObject.scale.x = parseFloat(v))); ui.scaleY.addEventListener('change', updateSel(v => selectedObject.scale.y = parseFloat(v))); ui.scaleZ.addEventListener('change', updateSel(v => selectedObject.scale.z = parseFloat(v))); | |
ui.color.addEventListener('input', updateSel(v => { if (selectedObject.material) { selectedObject.material.color.set(v); selectedObject.material.map = null; selectedObject.material.needsUpdate = true; selectedObject.userData.textureFile = null; selectedObject.userData.textureData = null; } })); | |
ui.texture.addEventListener('change', e => { if (selectedObject && e.target.files && e.target.files[0]) handleTextureLoad(e.target.files[0], (texture, file) => { if (selectedObject.material) { selectedObject.material.map = texture; selectedObject.material.needsUpdate = true; selectedObject.userData.textureFile = file.name.toUpperCase().replace(/\..+$/, ''); selectedObject.userData.textureData = file; } }); }); | |
ui.vdp2BgColor.addEventListener('input', e => { vdp2_settings.bgColor = e.target.value; if (!vdp2_settings.nbg1.texture) scene.background = new THREE.Color(vdp2_settings.bgColor); }); | |
ui.vdp2Nbg1Texture.addEventListener('change', e => { if (e.target.files && e.target.files[0]) handleTextureLoad(e.target.files[0], (texture, file) => { scene.background = texture; vdp2_settings.nbg1.texture = texture; vdp2_settings.nbg1.file = file; ui.vdp2Nbg1Controls.style.display = 'block'; }); }); | |
ui.vdp2Nbg1ScrollX.addEventListener('change', e => vdp2_settings.nbg1.scrollX = parseFloat(e.target.value)); ui.vdp2Nbg1ScrollY.addEventListener('change', e => vdp2_settings.nbg1.scrollY = parseFloat(e.target.value)); | |
ui.vdp2Nbg0Texture.addEventListener('change', e => { if (e.target.files && e.target.files[0]) handleTextureLoad(e.target.files[0], (texture, file) => { texture.wrapS = texture.wrapT = THREE.RepeatWrapping; vdp2_settings.nbg0.texture = texture; vdp2_settings.nbg0.file = file; updateVdp2InfinitePlane(); ui.vdp2Nbg0PlanePreview.style.display = 'block'; }); }); | |
ui.vdp2Nbg0PosY.addEventListener('change', e => { vdp2_settings.nbg0.posY = parseFloat(e.target.value); updateVdp2InfinitePlane(); }); ui.vdp2Nbg0Scale.addEventListener('change', e => { vdp2_settings.nbg0.scale = parseFloat(e.target.value); updateVdp2InfinitePlane(); }); | |
} | |
function handleTextureLoad(file, callback) { const reader = new FileReader(); reader.onload = (event) => new THREE.TextureLoader().load(event.target.result, (tex) => callback(tex, file)); reader.readAsDataURL(file); } | |
function updateVdp2InfinitePlane() { if (!vdp2_settings.nbg0.texture) return; if (!vdp2_infinite_plane) { const planeGeom = new THREE.PlaneGeometry(2000, 2000); const planeMat = new THREE.MeshStandardMaterial({ side: THREE.DoubleSide, transparent: true }); vdp2_infinite_plane = new THREE.Mesh(planeGeom, planeMat); vdp2_infinite_plane.rotation.x = -Math.PI / 2; vdp2_infinite_plane.raycast = () => {}; scene.add(vdp2_infinite_plane); } vdp2_infinite_plane.material.map = vdp2_settings.nbg0.texture; vdp2_infinite_plane.position.y = vdp2_settings.nbg0.posY; const texScale = 10 / vdp2_settings.nbg0.scale; vdp2_infinite_plane.material.map.repeat.set(texScale, texScale); vdp2_infinite_plane.material.needsUpdate = true; } | |
function rebuildCustomMeshFromData(group) { | |
while(group.children.length > 0) { const child = group.children[0]; group.remove(child); child.geometry.dispose(); child.material.dispose(); } | |
try { | |
const polygons = JSON.parse(group.userData.definition); | |
if (!Array.isArray(polygons)) throw new Error("A raiz do JSON deve ser um array de polígonos."); | |
polygons.forEach(poly => { | |
if (!poly.vertices || poly.vertices.length !== 4) return; | |
const geometry = new THREE.BufferGeometry(); | |
const positions = poly.vertices.flatMap(v => [v.x || 0, v.y || 0, v.z || 0]); | |
geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3)); | |
geometry.setIndex([0, 1, 2, 0, 2, 3]); | |
geometry.computeVertexNormals(); | |
const material = new THREE.MeshStandardMaterial({ color: poly.color || '#FFFFFF', side: THREE.DoubleSide }); | |
const mesh = new THREE.Mesh(geometry, material); | |
group.add(mesh); | |
}); | |
} catch (e) { console.error("Erro ao analisar o JSON da malha:", e); alert("Erro ao analisar o JSON da malha: " + e.message); } | |
} | |
function animate() { requestAnimationFrame(animate); orbitControls.update(); renderer.render(scene, camera); } | |
function showGeneratedCode() { const textures = new Map(); let textureIdCounter = 0; const addTexture = (file) => { if (!file) return; const name = file.name.toUpperCase().replace(/\..+$/, ''); if (textures.has(name)) return; textures.set(name, textureIdCounter++); }; addTexture(vdp2_settings.nbg1.file); addTexture(vdp2_settings.nbg0.file); objects.forEach(obj => addTexture(obj.userData.textureData)); const cCode = generateJoEngineCode(textures); ui.generatedCodeEl.textContent = cCode; hljs.highlightElement(ui.generatedCodeEl); ui.codeContainer.style.display = 'flex'; } | |
async function createTgaBlob(file) { return new Promise(resolve => { const img = new Image(); img.onload = () => { const canvas = document.createElement('canvas'); canvas.width = img.width; canvas.height = img.height; const ctx = canvas.getContext('2d'); ctx.drawImage(img, 0, 0); const imageData = ctx.getImageData(0, 0, img.width, img.height); const { data, width, height } = imageData; const header = new Uint8Array(18); header[2] = 2; header[12] = width & 0xFF; header[13] = (width >> 8) & 0xFF; header[14] = height & 0xFF; header[15] = (height >> 8) & 0xFF; header[16] = 32; header[17] = 0x28; const pixelData = new Uint8Array(width * height * 4); for (let i = 0; i < data.length; i += 4) { pixelData[i] = data[i+2]; pixelData[i+1] = data[i+1]; pixelData[i+2] = data[i]; pixelData[i+3] = data[i+3]; } resolve(new Blob([header, pixelData], { type: 'image/tga' })); }; img.src = URL.createObjectURL(file); }); } | |
async function generateProjectZip() { const zip = new JSZip(); const tgaFolder = zip.folder("TGA"); const textures = new Map(); let textureIdCounter = 0; const addTextureToZip = async (file) => { if (!file) return; const name = file.name.toUpperCase().replace(/\..+$/, ''); if (textures.has(name)) return; const tgaBlob = await createTgaBlob(file); tgaFolder.file(`${name}.TGA`, tgaBlob); textures.set(name, textureIdCounter++); }; await addTextureToZip(vdp2_settings.nbg1.file); await addTextureToZip(vdp2_settings.nbg0.file); for (const obj of objects) await addTextureToZip(obj.userData.textureData); const cCode = generateJoEngineCode(textures); zip.file("main.c", cCode); zip.generateAsync({ type: "blob" }).then(content => { const link = document.createElement('a'); link.href = URL.createObjectURL(content); link.download = "jo_engine_scene.zip"; link.click(); }); } | |
function generateJoEngineCode(textures) { | |
let declarations = `#include <jo/jo.h>\n\n`; | |
let creationCode = `void criar_cenario(void)\n{\n`; | |
let drawingCode = `void desenhar_cenario(void)\n{\n`; | |
let vblankCode = `void vblank_tasks(void)\n{\n`; | |
if (textures.size > 0) { | |
for (const [name, id] of textures.entries()) { | |
declarations += `#define TEXTURE_${name}_ID ${id}\n`; | |
} | |
declarations += `\n`; | |
} | |
objects.forEach(obj => { | |
declarations += `jo_3d_mesh* mesh_${obj.name};\n`; | |
}); | |
declarations += `\n`; | |
const c = new THREE.Color(vdp2_settings.bgColor); | |
creationCode += ` jo_set_default_background_color(JO_COLOR_RGB(${Math.round(c.r * 255)}, ${Math.round(c.g * 255)}, ${Math.round(c.b * 255)}));\n\n`; | |
if (textures.size > 0) { | |
for (const [name, id] of textures.entries()) { | |
creationCode += ` jo_sprite_add_tga("TGA", "${name}.TGA", JO_COLOR_Transparent, TEXTURE_${name}_ID);\n`; | |
} | |
creationCode += `\n`; | |
} | |
if (vdp2_settings.nbg1.file || vdp2_settings.nbg0.file) { | |
if (vdp2_settings.nbg1.file) { | |
const name = vdp2_settings.nbg1.file.name.toUpperCase().replace(/\..+$/, ''); | |
creationCode += ` jo_vdp2_add_scroll_screen(JO_VDP2_NBG1, TEXTURE_${name}_ID, JO_VDP2_SCROLL_PLANE_A);\n`; | |
vblankCode += ` jo_vdp2_set_scroll(JO_VDP2_NBG1, jo_float2fixed(${vdp2_settings.nbg1.scrollX.toFixed(4)}), jo_float2fixed(${vdp2_settings.nbg1.scrollY.toFixed(4)}));\n`; | |
} | |
if (vdp2_settings.nbg0.file) { | |
const name = vdp2_settings.nbg0.file.name.toUpperCase().replace(/\..+$/, ''); | |
creationCode += ` jo_vdp2_add_infinite_scroll(JO_VDP2_NBG0, TEXTURE_${name}_ID, JO_VDP2_SCROLL_PLANE_A);\n`; | |
creationCode += ` jo_vdp2_set_infinite_scroll_y(JO_VDP2_NBG0, ${Math.round(vdp2_settings.nbg0.posY)});\n`; | |
creationCode += ` jo_vdp2_set_infinite_scroll_scale(JO_VDP2_NBG0, jo_float2fixed(${vdp2_settings.nbg0.scale.toFixed(4)}));\n`; | |
} | |
creationCode += `\n`; | |
} | |
objects.forEach(obj => { | |
const name = obj.name; | |
if (obj.userData.type === 'cube') { | |
creationCode += ` mesh_${name} = jo_3d_create_mesh(6);\n`; | |
creationCode += ` jo_3d_map_cube(mesh_${name});\n`; | |
if (obj.userData.textureFile) { | |
creationCode += ` jo_3d_set_mesh_texture(mesh_${name}, TEXTURE_${obj.userData.textureFile}_ID);\n`; | |
} else { | |
const c = obj.material.color; | |
creationCode += ` jo_3d_set_mesh_color(mesh_${name}, JO_COLOR_RGB(${Math.round(c.r * 255)}, ${Math.round(c.g * 255)}, ${Math.round(c.b * 255)}));\n`; | |
} | |
} else if (obj.userData.type === 'plane') { | |
creationCode += ` mesh_${name} = jo_3d_create_mesh(1);\n`; | |
creationCode += ` jo_3d_map_plane(mesh_${name});\n`; | |
if (obj.userData.textureFile) { | |
creationCode += ` jo_3d_set_mesh_texture(mesh_${name}, TEXTURE_${obj.userData.textureFile}_ID);\n`; | |
} else { | |
const c = obj.material.color; | |
creationCode += ` jo_3d_set_mesh_color(mesh_${name}, JO_COLOR_RGB(${Math.round(c.r * 255)}, ${Math.round(c.g * 255)}, ${Math.round(c.b * 255)}));\n`; | |
} | |
} else if (obj.userData.type === 'custom_mesh') { | |
try { | |
const polygons = JSON.parse(obj.userData.definition); | |
creationCode += ` jo_vertice vertices_${name}[${polygons.length * 4}];\n`; | |
let vertexCount = 0; | |
polygons.forEach(poly => { | |
poly.vertices.forEach(v => { | |
creationCode += ` vertices_${name}[${vertexCount}].pos[X] = jo_int2fixed(${v.x || 0});\n`; | |
creationCode += ` vertices_${name}[${vertexCount}].pos[Y] = jo_int2fixed(${v.y || 0});\n`; | |
creationCode += ` vertices_${name}[${vertexCount}].pos[Z] = jo_int2fixed(${v.z || 0});\n`; | |
vertexCount++; | |
}); | |
}); | |
creationCode += ` mesh_${name} = jo_3d_create_mesh_from_vertices(${polygons.length}, vertices_${name});\n`; | |
polygons.forEach((poly, polyIndex) => { | |
const color = new THREE.Color(poly.color); | |
creationCode += ` jo_3d_set_mesh_polygon_color(mesh_${name}, JO_COLOR_RGB(${Math.round(color.r*255)}, ${Math.round(color.g*255)}, ${Math.round(color.b*255)}), ${polyIndex});\n`; | |
}); | |
} catch(e) { creationCode += ` // ERRO NA MALHA ${name}\n`; } | |
} | |
creationCode += `\n`; | |
const pos = obj.position; | |
const scale = obj.scale; | |
drawingCode += ` jo_3d_push_matrix();\n {\n`; | |
drawingCode += ` jo_3d_translate_matrix_fixed(jo_int2fixed(${Math.round(pos.x)}), jo_int2fixed(${Math.round(pos.y)}), jo_int2fixed(${Math.round(pos.z)}));\n`; | |
drawingCode += ` jo_3d_rotate_matrix_rad(${obj.rotation.x.toFixed(4)}, ${obj.rotation.y.toFixed(4)}, ${obj.rotation.z.toFixed(4)});\n`; | |
if (obj.userData.type === 'cube' || obj.userData.type === 'plane') { | |
const baseSize = obj.userData.baseSize; | |
const sx = scale.x * (baseSize / 2); | |
const sy = scale.y * (baseSize / 2); | |
const sz = scale.z * (baseSize / 2); | |
// The jo_3d_map_cube/plane creates a 2x2x2 unit primitive, so we scale it by (size/2) | |
drawingCode += ` jo_3d_scale_matrix(jo_int2fixed(${Math.round(sx)}), jo_int2fixed(${Math.round(sy)}), jo_int2fixed(${Math.round(sz)}));\n`; | |
} else { | |
// Custom meshes are built with exact coordinates, so we scale directly | |
drawingCode += ` jo_3d_scale_matrix(jo_float2fixed(${scale.x.toFixed(4)}), jo_float2fixed(${scale.y.toFixed(4)}), jo_float2fixed(${scale.z.toFixed(4)}));\n`; | |
} | |
drawingCode += ` jo_3d_mesh_draw(mesh_${name});\n }\n jo_3d_pop_matrix();\n\n`; | |
}); | |
creationCode += `}\n`; | |
drawingCode += `}\n`; | |
vblankCode += `}\n`; | |
let mainCode = `\nvoid jo_main(void)\n{\n jo_core_init(JO_COLOR_Black);\n jo_vdp2_init();\n\n jo_camera camera;\n jo_3d_camera_init(&camera);\n jo_3d_camera_set_viewpoint(&camera, 0, 40, 250);\n jo_3d_camera_set_target(&camera, 0, 0, 0);\n jo_3d_camera_look_at(&camera);\n\n criar_cenario();\n\n jo_core_add_vblank_callback(vblank_tasks);\n jo_core_add_callback(desenhar_cenario);\n jo_core_run();\n}\n`; | |
return declarations + creationCode + '\n' + drawingCode + '\n' + vblankCode + '\n' + mainCode; | |
} | |
init(); | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment