Skip to content

Instantly share code, notes, and snippets.

@celsowm
Last active July 19, 2025 22:40
Show Gist options
  • Save celsowm/35fd29907f72088ef1972a2b7da3a021 to your computer and use it in GitHub Desktop.
Save celsowm/35fd29907f72088ef1972a2b7da3a021 to your computer and use it in GitHub Desktop.
Jo Engine 3D editor and generator code tool
<!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