// ============================================================ // LENS CAMERA v1.0 — Simulação de Lentes Óticas para o NodeRig // Módulo independente: sem modificações em app.js // ============================================================ (function () { "use strict"; // ============================================================ // ESTADO // ============================================================ var focalLength = 50; // mm (padrão: 50mm "nifty fifty") var distortionK = 0.0; // barrel distortion (0 = nenhum) var blurAmount = 0.0; // bokeh amount (0–1) var vignetteAmount = 0.0; // vignette (0–1) var caAmount = 0.0; // chromatic aberration (0–1) var lensMode = "normal"; var isPostEnabled = false; var renderTarget = null; var postScene = null; var postCamera = null; var postMaterial = null; var _origRender = null; // Presets de focal (mm) var FOCAL_PRESETS = [14, 24, 35, 50, 85, 135, 200]; // Presets por modo de lente var LENS_MODES = { normal: { focal: 50, distortion: 0.00 }, angular: { focal: 24, distortion: 0.10 }, fisheye: { focal: 14, distortion: 0.42 } }; // ============================================================ // CÁLCULOS ÓPTICOS // ============================================================ /** * Converte distância focal (mm) em FOV vertical (graus) * usando sensor full-frame 35mm (36×24mm). */ function fovFromFocal(mm) { var sensorH = 24.0; // mm (altura do sensor) return 2.0 * Math.atan(sensorH / (2.0 * mm)) * (180.0 / Math.PI); } /** * Converte valor do slider de blur (0–1) em f-stop equivalente. * 0 = f/16 (fechado / nítido), 1 = f/1.2 (aberto / bokeh). */ function blurToFstop(v) { var logMin = Math.log(1.2); var logMax = Math.log(16.0); var logF = logMax - v * (logMax - logMin); return "f/" + Math.exp(logF).toFixed(1); } // ============================================================ // SHADERS GLSL // ============================================================ var VERT = [ "varying vec2 vUv;", "void main() {", " vUv = uv;", " gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);", "}" ].join("\n"); var FRAG = [ "uniform sampler2D tDiffuse;", "uniform float uDistortion;", "uniform float uBlur;", "uniform float uVignette;", "uniform float uCA;", "varying vec2 vUv;", "", "// Barrel / fisheye distortion", "vec2 barrel(vec2 uv, float k) {", " vec2 c = uv * 2.0 - 1.0;", " float r2 = dot(c, c);", " c *= 1.0 + k * r2 + k * 0.15 * r2 * r2;", " return c * 0.5 + 0.5;", "}", "", "void main() {", " vec2 dUv = (uDistortion > 0.001) ? barrel(vUv, uDistortion) : vUv;", "", " // Fora dos limites → preto (crop circular do fisheye)", " if (dUv.x < 0.0 || dUv.x > 1.0 || dUv.y < 0.0 || dUv.y > 1.0) {", " gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0);", " return;", " }", "", " // 12-tap Poisson disk bokeh", " float br = uBlur * 0.065;", " vec4 color;", " if (br > 0.0001) {", " color = texture2D(tDiffuse, dUv + br * vec2(-0.326,-0.406));", " color += texture2D(tDiffuse, dUv + br * vec2(-0.840,-0.074));", " color += texture2D(tDiffuse, dUv + br * vec2(-0.696, 0.457));", " color += texture2D(tDiffuse, dUv + br * vec2(-0.203, 0.621));", " color += texture2D(tDiffuse, dUv + br * vec2( 0.962,-0.195));", " color += texture2D(tDiffuse, dUv + br * vec2( 0.473,-0.480));", " color += texture2D(tDiffuse, dUv + br * vec2( 0.519, 0.767));", " color += texture2D(tDiffuse, dUv + br * vec2( 0.185,-0.893));", " color += texture2D(tDiffuse, dUv + br * vec2( 0.507, 0.064));", " color += texture2D(tDiffuse, dUv + br * vec2( 0.896, 0.412));", " color += texture2D(tDiffuse, dUv + br * vec2(-0.322,-0.933));", " color += texture2D(tDiffuse, dUv + br * vec2(-0.792, 0.716));", " color /= 12.0;", " } else {", " color = texture2D(tDiffuse, dUv);", " }", "", " // Aberração cromática — desfasagem radial RGB", " if (uCA > 0.001) {", " vec2 toCenter = 0.5 - dUv;", " float ca = uCA * 0.022;", " color.r = texture2D(tDiffuse, dUv - toCenter * ca).r;", " color.b = texture2D(tDiffuse, dUv + toCenter * ca).b;", " }", "", " // Vinheta", " if (uVignette > 0.001) {", " float d = length(vUv * 2.0 - 1.0);", " color.rgb *= 1.0 - smoothstep(0.25, 1.35, d) * uVignette;", " }", "", " gl_FragColor = color;", "}" ].join("\n"); // ============================================================ // POST-PROCESS // ============================================================ function getRTSize() { return { w: renderer.domElement.width || window.innerWidth, h: renderer.domElement.height || window.innerHeight }; } function initPostProcess() { if (renderTarget) return; var sz = getRTSize(); renderTarget = new THREE.WebGLRenderTarget(sz.w, sz.h, { minFilter: THREE.LinearFilter, magFilter: THREE.LinearFilter, format: THREE.RGBAFormat }); postCamera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1); postScene = new THREE.Scene(); postMaterial = new THREE.ShaderMaterial({ vertexShader: VERT, fragmentShader: FRAG, uniforms: { tDiffuse: { value: renderTarget.texture }, uDistortion: { value: 0.0 }, uBlur: { value: 0.0 }, uVignette: { value: 0.0 }, uCA: { value: 0.0 } }, depthTest: false, depthWrite: false }); var PlaneGeo = THREE.PlaneGeometry || THREE.PlaneBufferGeometry; postScene.add(new THREE.Mesh(new PlaneGeo(2, 2), postMaterial)); // Intercepta renderer.render — zero-cost quando desativado _origRender = renderer.render.bind(renderer); renderer.render = function (sc, cam) { if (isPostEnabled) { renderer.setRenderTarget(renderTarget); _origRender(sc, cam); renderer.setRenderTarget(null); _origRender(postScene, postCamera); } else { _origRender(sc, cam); } }; console.log("[LensCamera] Post-process pronto: " + sz.w + "×" + sz.h); } function updateRTSize() { if (!renderTarget) return; var sz = getRTSize(); renderTarget.setSize(sz.w, sz.h); } function syncUniforms() { if (!postMaterial) return; postMaterial.uniforms.uDistortion.value = distortionK; postMaterial.uniforms.uBlur.value = blurAmount; postMaterial.uniforms.uVignette.value = vignetteAmount; postMaterial.uniforms.uCA.value = caAmount; } function checkPostProcess() { isPostEnabled = distortionK > 0.001 || blurAmount > 0.001 || vignetteAmount > 0.001 || caAmount > 0.001; if (isPostEnabled && !renderTarget) initPostProcess(); if (renderTarget) syncUniforms(); } // ============================================================ // APLICA FOCAL / MODO // ============================================================ function applyFocalLength(mm) { focalLength = Math.max(14, Math.min(200, mm)); camera.fov = fovFromFocal(focalLength); camera.updateProjectionMatrix(); refreshUI(); } function applyLensMode(mode) { lensMode = mode; var p = LENS_MODES[mode]; distortionK = p.distortion; applyFocalLength(p.focal); // Sliders setSliderVal("lens-focal-slider", p.focal); setSliderVal("lens-distortion-slider", p.distortion); // Distortion row: ocultar no modo Normal var distRow = document.getElementById("lens-distortion-row"); if (distRow) distRow.style.display = (mode === "normal") ? "none" : ""; // Botões de tipo ["normal", "angular", "fisheye"].forEach(function (m) { toggleClass("lens-type-" + m, "active", m === mode); }); checkPostProcess(); refreshUI(); } // ============================================================ // ATUALIZA DISPLAY // ============================================================ function refreshUI() { var fov = fovFromFocal(focalLength); setText("lens-focal-val", focalLength + "mm • " + fov.toFixed(1) + "°"); setText("lens-distortion-val", distortionK.toFixed(2)); setText("lens-blur-val", blurToFstop(blurAmount) + " (" + blurAmount.toFixed(2) + ")"); setText("lens-vignette-val", vignetteAmount.toFixed(2)); setText("lens-ca-val", caAmount.toFixed(2)); var modeLabel = { normal: "Normal", angular: "Angular", fisheye: "Fisheye" }; setText("lens-header-info", focalLength + "mm · " + (modeLabel[lensMode] || "")); // Chips de focal FOCAL_PRESETS.forEach(function (mm) { toggleClass("lens-preset-" + mm, "active", Math.abs(focalLength - mm) < 1); }); } // ============================================================ // RESET // ============================================================ function resetLens() { focalLength = 50; distortionK = 0.0; blurAmount = 0.0; vignetteAmount = 0.0; caAmount = 0.0; lensMode = "normal"; isPostEnabled = false; applyFocalLength(50); var sliders = { "lens-focal-slider": 50, "lens-distortion-slider": 0, "lens-blur-slider": 0, "lens-vignette-slider": 0, "lens-ca-slider": 0 }; Object.keys(sliders).forEach(function (id) { setSliderVal(id, sliders[id]); }); ["normal", "angular", "fisheye"].forEach(function (m) { toggleClass("lens-type-" + m, "active", m === "normal"); }); var distRow = document.getElementById("lens-distortion-row"); if (distRow) distRow.style.display = "none"; if (postMaterial) syncUniforms(); refreshUI(); if (typeof showStatus === "function") showStatus("Lente resetada para padrão (50mm)"); } // ============================================================ // PAINEL DE CONTROLE // ============================================================ function buildPresetChips() { return FOCAL_PRESETS.map(function (mm) { var active = mm === 50 ? " active" : ""; return ''; }).join(""); } function createLensPanel() { if (document.getElementById("lens-panel")) return; var panel = document.createElement("div"); panel.id = "lens-panel"; panel.className = "lens-panel"; panel.innerHTML = // ── Header ────────────────────────────────────────── '
' + '📷 Lente' + '50mm · Normal' + '
' + // ── Distância Focal ────────────────────────────────── '
' + '
Distância Focal
' + '
' + buildPresetChips() + '
' + '
' + '' + '50mm • 19.0°' + '
' + '
' + // ── Tipo de Lente ───────────────────────────────────── '
' + '
Tipo de Lente
' + '
' + '' + '' + '' + '
' + '
' + // ── Distorção (oculta no modo Normal) ──────────────── '' + // ── Bokeh / Abertura ────────────────────────────────── '
' + '
' + '
' + 'Abertura (Bokeh)' + 'f/16 (0.00)' + '
' + '
f/16 Sharpf/1.2 Aberto
' + '' + '
' + '
' + // ── Vinheta ─────────────────────────────────────────── '
' + '
' + '
' + 'Vinheta' + '0.00' + '
' + '' + '
' + '
' + // ── Aberração Cromática ─────────────────────────────── '
' + '
' + '
' + 'Aberração Cromática' + '0.00' + '
' + '' + '
' + '
' + // ── Reset ───────────────────────────────────────────── '
' + '' + '
'; document.body.appendChild(panel); wireListeners(panel); } function wireListeners(panel) { // Focal presets FOCAL_PRESETS.forEach(function (mm) { on("lens-preset-" + mm, "click", function () { applyFocalLength(mm); }); }); // Focal slider on("lens-focal-slider", "input", function () { applyFocalLength(parseInt(this.value, 10)); }); // Lens type buttons ["normal", "angular", "fisheye"].forEach(function (mode) { on("lens-type-" + mode, "click", function () { applyLensMode(mode); }); }); // Distortion slider on("lens-distortion-slider", "input", function () { distortionK = parseFloat(this.value); setText("lens-distortion-val", distortionK.toFixed(2)); checkPostProcess(); }); // Blur slider on("lens-blur-slider", "input", function () { blurAmount = parseFloat(this.value); checkPostProcess(); refreshUI(); }); // Vignette slider on("lens-vignette-slider", "input", function () { vignetteAmount = parseFloat(this.value); checkPostProcess(); setText("lens-vignette-val", vignetteAmount.toFixed(2)); }); // CA slider on("lens-ca-slider", "input", function () { caAmount = parseFloat(this.value); checkPostProcess(); setText("lens-ca-val", caAmount.toFixed(2)); }); // Reset on("lens-reset-btn", "click", resetLens); // Bloqueia propagação de eventos para dock/orbit panel.addEventListener("pointerdown", function (e) { e.stopPropagation(); }); panel.addEventListener("mousedown", function (e) { e.stopPropagation(); }); // Atualiza RenderTarget quando resolução ou janela mudam on("res-selector", "change", function () { setTimeout(updateRTSize, 150); }); window.addEventListener("resize", function () { setTimeout(updateRTSize, 150); }); } // ============================================================ // HELPERS DOM // ============================================================ function on(id, evt, fn) { var el = document.getElementById(id); if (el) el.addEventListener(evt, fn); } function setText(id, text) { var el = document.getElementById(id); if (el) el.textContent = text; } function setSliderVal(id, val) { var el = document.getElementById(id); if (el) el.value = val; } function toggleClass(id, cls, state) { var el = document.getElementById(id); if (el) el.classList.toggle(cls, state); } // ============================================================ // API GLOBAL // ============================================================ window.LensCamera = { setFocal: applyFocalLength, setMode: applyLensMode, reset: resetLens, isActive: function () { return isPostEnabled; }, getFocal: function () { return focalLength; }, getFOV: function () { return fovFromFocal(focalLength); } }; // ============================================================ // INICIALIZAÇÃO // ============================================================ function init() { createLensPanel(); applyFocalLength(50); // Padrão: 50mm console.log("[LensCamera] Sistema de lentes v1.0 pronto."); } if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", init); } else { setTimeout(init, 250); // Após app.js + skeletonRig.js } })();