renderang v0.1: PS1/N64/Saturn retro renderer — vertex snap, affine warp, low-res framebuffer, color crush + Bayer dither, animated GLTF demo actor
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
commit
38857c7fc1
6
.claude/launch.json
Normal file
6
.claude/launch.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"version": "0.0.1",
|
||||
"configurations": [
|
||||
{ "name": "renderang", "runtimeExecutable": "python3", "runtimeArgs": ["-m", "http.server", "8930"], "port": 8930 }
|
||||
]
|
||||
}
|
||||
22
README.md
Normal file
22
README.md
Normal file
@ -0,0 +1,22 @@
|
||||
# renderang
|
||||
|
||||
A retro console renderer for the GODVERSE — PS1 / Saturn / N64 looks over three.js.
|
||||
|
||||
- **Vertex snapping** (quantized NDC = the PS1 wobble), **affine texture warp**,
|
||||
low-res internal framebuffer with integer nearest upscale, 15-bit color crush,
|
||||
4x4 Bayer dither, scanlines, fog. All live-tweakable.
|
||||
- Works on stock three.js materials via `onBeforeCompile`, so skinned/animated
|
||||
GLTF characters work untouched — demo actor is a quaternius modular man
|
||||
(CC0) with his 24 embedded clips.
|
||||
- No build step: importmap + vendored three (r170). Serve the dir and go:
|
||||
`python3 -m http.server 8930`
|
||||
|
||||
## files
|
||||
- `src/retro.js` — material patcher (`patchRetroMaterial`, `retrofyObject`) + `RetroScreen` post pipeline
|
||||
- `src/main.js` — demo scene, mode presets (PS1/N64/SATURN/OFF), UI wiring
|
||||
- `assets/suit.glb` — demo actor (quaternius, CC0)
|
||||
|
||||
## next ideas
|
||||
- level loader (bring in cosmos/kitbash exports)
|
||||
- CRT curvature + phosphor mask pass
|
||||
- MRPGI / web-game integration as a drop-in renderer
|
||||
BIN
assets/suit.glb
Normal file
BIN
assets/suit.glb
Normal file
Binary file not shown.
76
index.html
Normal file
76
index.html
Normal file
@ -0,0 +1,76 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>renderang</title>
|
||||
<style>
|
||||
:root { --bg:#14101d; --panel:#241c33; --ink:#cfc3ec; --hot:#ff5c8a; --dim:#6b5f8a; }
|
||||
* { box-sizing: border-box; }
|
||||
body { margin:0; background:var(--bg); color:var(--ink);
|
||||
font-family:"Courier New", monospace; height:100vh; display:flex; overflow:hidden; }
|
||||
#view { flex:1; display:flex; align-items:center; justify-content:center; background:#000; }
|
||||
#view canvas { image-rendering: pixelated; }
|
||||
#panel { width:230px; background:var(--panel); border-left:4px solid #000;
|
||||
padding:10px; font-size:12px; overflow-y:auto; flex-shrink:0; }
|
||||
h1 { font-size:15px; margin:0 0 2px; color:var(--hot); letter-spacing:2px; }
|
||||
.sub { color:var(--dim); margin-bottom:12px; }
|
||||
fieldset { border:2px solid var(--dim); margin:0 0 10px; padding:6px 8px; }
|
||||
legend { padding:0 4px; color:var(--hot); }
|
||||
label { display:block; margin:6px 0 2px; }
|
||||
input[type=range] { width:100%; accent-color:var(--hot); }
|
||||
select { width:100%; background:var(--bg); color:var(--ink);
|
||||
border:2px solid var(--dim); font-family:inherit; padding:2px; }
|
||||
.modes { display:grid; grid-template-columns:1fr 1fr; gap:4px; }
|
||||
.modes button { background:var(--bg); color:var(--ink); border:2px solid var(--dim);
|
||||
padding:5px 0; font-family:inherit; font-size:12px; cursor:pointer; }
|
||||
.modes button.on { background:var(--hot); color:#14101d; border-color:var(--hot); font-weight:bold; }
|
||||
.row { display:flex; align-items:center; gap:6px; margin:5px 0; }
|
||||
.row input { accent-color:var(--hot); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="view"></div>
|
||||
<div id="panel">
|
||||
<h1>RENDERANG</h1>
|
||||
<div class="sub">retro renderer v0.1</div>
|
||||
|
||||
<fieldset>
|
||||
<legend>mode</legend>
|
||||
<div class="modes">
|
||||
<button data-mode="ps1" class="on">PS1</button>
|
||||
<button data-mode="n64">N64</button>
|
||||
<button data-mode="saturn">SATURN</button>
|
||||
<button data-mode="off">OFF</button>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend>knobs</legend>
|
||||
<label>vertex snap <input id="snap" type="range" min="0" max="1" step="0.05" value="1"></label>
|
||||
<label>affine warp <input id="affine" type="range" min="0" max="1" step="0.05" value="1"></label>
|
||||
<label>color levels <input id="levels" type="range" min="4" max="256" step="4" value="32"></label>
|
||||
<div class="row"><input id="dither" type="checkbox" checked><span>bayer dither</span></div>
|
||||
<div class="row"><input id="scan" type="checkbox"><span>scanlines</span></div>
|
||||
<label>internal res
|
||||
<select id="res">
|
||||
<option>320x240</option>
|
||||
<option>256x224</option>
|
||||
<option>160x120</option>
|
||||
<option>512x384</option>
|
||||
<option>640x480</option>
|
||||
</select>
|
||||
</label>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend>actor</legend>
|
||||
<label>animation <select id="anim"></select></label>
|
||||
</fieldset>
|
||||
</div>
|
||||
<script type="importmap">
|
||||
{ "imports": { "three": "./vendor/three.module.js" } }
|
||||
</script>
|
||||
<script type="module" src="src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
190
src/main.js
Normal file
190
src/main.js
Normal file
@ -0,0 +1,190 @@
|
||||
// renderang demo — a quaternius character in a PS1-flavoured world.
|
||||
import * as THREE from 'three';
|
||||
import { GLTFLoader } from '../vendor/GLTFLoader.js';
|
||||
import { OrbitControls } from '../vendor/OrbitControls.js';
|
||||
import { RetroScreen, retroState, retrofyObject, patchRetroMaterial } from './retro.js';
|
||||
|
||||
const FOG_COLOR = 0x2a2036;
|
||||
|
||||
const renderer = new THREE.WebGLRenderer({ antialias: false });
|
||||
renderer.setPixelRatio(1);
|
||||
document.getElementById('view').appendChild(renderer.domElement);
|
||||
|
||||
const scene = new THREE.Scene();
|
||||
scene.background = new THREE.Color(FOG_COLOR);
|
||||
scene.fog = new THREE.Fog(FOG_COLOR, 6, 26);
|
||||
|
||||
const camera = new THREE.PerspectiveCamera(60, 4 / 3, 0.1, 100);
|
||||
camera.position.set(3.2, 2.2, 4.2);
|
||||
const controls = new OrbitControls(camera, renderer.domElement);
|
||||
controls.target.set(0, 1, 0);
|
||||
controls.enableDamping = true;
|
||||
|
||||
scene.add(new THREE.HemisphereLight(0xcabfe8, 0x4a3c5e, 1.6));
|
||||
const sun = new THREE.DirectionalLight(0xffeedd, 2.2);
|
||||
sun.position.set(4, 6, 3);
|
||||
scene.add(sun);
|
||||
|
||||
const screen = new RetroScreen(renderer);
|
||||
|
||||
// --- procedural retro textures (canvas, nearest) ------------------------------
|
||||
function canvasTexture(draw, size = 64) {
|
||||
const c = document.createElement('canvas');
|
||||
c.width = c.height = size;
|
||||
draw(c.getContext('2d'), size);
|
||||
const t = new THREE.CanvasTexture(c);
|
||||
t.magFilter = THREE.NearestFilter;
|
||||
t.minFilter = THREE.NearestFilter;
|
||||
t.generateMipmaps = false;
|
||||
t.wrapS = t.wrapT = THREE.RepeatWrapping;
|
||||
return t;
|
||||
}
|
||||
const checkerTex = canvasTexture((g, s) => {
|
||||
for (let y = 0; y < 8; y++) for (let x = 0; x < 8; x++) {
|
||||
g.fillStyle = (x + y) % 2 ? '#4a3f66' : '#5d5080';
|
||||
g.fillRect(x * s / 8, y * s / 8, s / 8, s / 8);
|
||||
}
|
||||
});
|
||||
checkerTex.repeat.set(12, 12);
|
||||
const brickTex = canvasTexture((g, s) => {
|
||||
g.fillStyle = '#6e5a4e'; g.fillRect(0, 0, s, s);
|
||||
g.fillStyle = '#8a7263';
|
||||
for (let y = 0; y < 4; y++) for (let x = 0; x < 4; x++)
|
||||
g.fillRect(x * 16 + (y % 2 ? 8 : 0) + 1, y * 16 + 1, 14, 14);
|
||||
});
|
||||
|
||||
const ground = new THREE.Mesh(
|
||||
new THREE.PlaneGeometry(40, 40),
|
||||
patchRetroMaterial(new THREE.MeshLambertMaterial({ map: checkerTex }))
|
||||
);
|
||||
ground.rotation.x = -Math.PI / 2;
|
||||
scene.add(ground);
|
||||
|
||||
// scatter some chunky geometry so the wobble has something to chew on
|
||||
const brickMat = patchRetroMaterial(new THREE.MeshLambertMaterial({ map: brickTex }));
|
||||
const shapes = [];
|
||||
for (let i = 0; i < 14; i++) {
|
||||
const kind = i % 3;
|
||||
const geo = kind === 0 ? new THREE.BoxGeometry(1.4, 1 + (i % 4), 1.4)
|
||||
: kind === 1 ? new THREE.ConeGeometry(0.9, 1.8, 6)
|
||||
: new THREE.CylinderGeometry(0.6, 0.6, 1.4, 8);
|
||||
const m = new THREE.Mesh(geo, brickMat);
|
||||
const a = (i / 14) * Math.PI * 2;
|
||||
const r = 6 + (i % 3) * 3;
|
||||
m.position.set(Math.cos(a) * r, geo.parameters.height ? geo.parameters.height / 2 : 0.7, Math.sin(a) * r);
|
||||
m.rotation.y = a;
|
||||
scene.add(m);
|
||||
shapes.push(m);
|
||||
}
|
||||
|
||||
// --- character ----------------------------------------------------------------
|
||||
let mixer = null;
|
||||
let actions = {};
|
||||
let current = null;
|
||||
new GLTFLoader().load('assets/suit.glb', (gltf) => {
|
||||
const char = retrofyObject(gltf.scene);
|
||||
scene.add(char);
|
||||
mixer = new THREE.AnimationMixer(char);
|
||||
const sel = document.getElementById('anim');
|
||||
gltf.animations
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
.forEach((clip) => {
|
||||
actions[clip.name] = mixer.clipAction(clip);
|
||||
const o = document.createElement('option');
|
||||
o.value = o.textContent = clip.name;
|
||||
sel.appendChild(o);
|
||||
});
|
||||
play('Walk');
|
||||
sel.value = 'Walk';
|
||||
sel.onchange = () => play(sel.value);
|
||||
});
|
||||
function play(name) {
|
||||
if (!actions[name]) return;
|
||||
if (current) current.fadeOut(0.15);
|
||||
current = actions[name].reset().fadeIn(0.15).play();
|
||||
}
|
||||
|
||||
// --- modes + UI ---------------------------------------------------------------
|
||||
const MODES = {
|
||||
ps1: { res: [320, 240], snap: 1, affine: 1, levels: 32, dither: 1, filter: 'nearest' },
|
||||
n64: { res: [320, 240], snap: 0, affine: 0, levels: 256, dither: 0.5, filter: 'linear' },
|
||||
saturn: { res: [352, 240], snap: 1, affine: 1, levels: 16, dither: 1, filter: 'nearest' },
|
||||
off: { res: null },
|
||||
};
|
||||
let retroOn = true;
|
||||
|
||||
function setTextureFilter(linear) {
|
||||
scene.traverse((o) => {
|
||||
if (o.isMesh && o.material && o.material.map) {
|
||||
const f = linear ? THREE.LinearFilter : THREE.NearestFilter;
|
||||
o.material.map.magFilter = f;
|
||||
o.material.map.minFilter = f;
|
||||
o.material.map.needsUpdate = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function applyMode(name) {
|
||||
const m = MODES[name];
|
||||
document.querySelectorAll('[data-mode]').forEach((b) =>
|
||||
b.classList.toggle('on', b.dataset.mode === name));
|
||||
if (!m.res) { retroOn = false; resize(); return; }
|
||||
retroOn = true;
|
||||
retroState.snap = m.snap;
|
||||
retroState.affine = m.affine;
|
||||
retroState.colorLevels = m.levels;
|
||||
retroState.dither = m.dither;
|
||||
screen.setResolution(m.res[0], m.res[1]);
|
||||
setTextureFilter(m.filter === 'linear');
|
||||
syncSliders();
|
||||
resize();
|
||||
}
|
||||
document.querySelectorAll('[data-mode]').forEach((b) =>
|
||||
b.addEventListener('click', () => applyMode(b.dataset.mode)));
|
||||
|
||||
const $ = (id) => document.getElementById(id);
|
||||
function syncSliders() {
|
||||
$('snap').value = retroState.snap;
|
||||
$('affine').value = retroState.affine;
|
||||
$('levels').value = retroState.colorLevels;
|
||||
$('dither').checked = retroState.dither > 0;
|
||||
$('scan').checked = retroState.scanlines > 0;
|
||||
}
|
||||
$('snap').oninput = (e) => (retroState.snap = +e.target.value);
|
||||
$('affine').oninput = (e) => (retroState.affine = +e.target.value);
|
||||
$('levels').oninput = (e) => (retroState.colorLevels = +e.target.value);
|
||||
$('dither').onchange = (e) => (retroState.dither = e.target.checked ? 1 : 0);
|
||||
$('scan').onchange = (e) => (retroState.scanlines = e.target.checked ? 1 : 0);
|
||||
$('res').onchange = (e) => {
|
||||
const [w, h] = e.target.value.split('x').map(Number);
|
||||
screen.setResolution(w, h);
|
||||
resize();
|
||||
};
|
||||
|
||||
// --- sizing: integer-scale the internal buffer into the window, letterboxed ----
|
||||
function resize() {
|
||||
const el = document.getElementById('view');
|
||||
const W = el.clientWidth, H = el.clientHeight;
|
||||
if (!retroOn) {
|
||||
renderer.setSize(W, H);
|
||||
camera.aspect = W / H;
|
||||
} else {
|
||||
const r = retroState.resolution;
|
||||
const scale = Math.max(1, Math.floor(Math.min(W / r.x, H / r.y)));
|
||||
renderer.setSize(r.x * scale, r.y * scale);
|
||||
camera.aspect = r.x / r.y;
|
||||
}
|
||||
camera.updateProjectionMatrix();
|
||||
}
|
||||
window.addEventListener('resize', resize);
|
||||
|
||||
applyMode('ps1');
|
||||
|
||||
const clock = new THREE.Clock();
|
||||
(function tick() {
|
||||
requestAnimationFrame(tick);
|
||||
const dt = clock.getDelta();
|
||||
if (mixer) mixer.update(dt);
|
||||
controls.update();
|
||||
screen.render(scene, camera, retroOn);
|
||||
})();
|
||||
193
src/retro.js
Normal file
193
src/retro.js
Normal file
@ -0,0 +1,193 @@
|
||||
// retro.js — the renderang retro pipeline.
|
||||
// Two halves:
|
||||
// 1. patchRetroMaterial(material) — vertex snapping + affine texture mapping,
|
||||
// injected into stock three.js materials so skinning/animation keep working.
|
||||
// 2. RetroScreen — renders the scene into a tiny framebuffer, then upscales it
|
||||
// with color crush, Bayer dither and optional scanlines.
|
||||
import * as THREE from 'three';
|
||||
|
||||
// Shared knobs — mutated live by the UI, read by shaders each frame via uniforms.
|
||||
export const retroState = {
|
||||
resolution: new THREE.Vector2(320, 240), // internal framebuffer
|
||||
snap: 1.0, // 0 = off, 1 = snap to internal pixel grid (PS1 wobble)
|
||||
affine: 1.0, // 0 = perspective correct, 1 = affine warp (PS1 textures)
|
||||
colorLevels: 32, // quantization levels per channel (32 = 15-bit PS1 vibe)
|
||||
dither: 1.0, // 0/1 Bayer dither
|
||||
scanlines: 0.0, // 0/1 CRT scanlines on the upscale
|
||||
pixelate: true, // nearest-neighbour upscale
|
||||
};
|
||||
|
||||
const materialUniforms = {
|
||||
uResolution: { value: retroState.resolution },
|
||||
uSnap: { value: retroState.snap },
|
||||
uAffine: { value: retroState.affine },
|
||||
};
|
||||
|
||||
export function updateRetroUniforms() {
|
||||
materialUniforms.uSnap.value = retroState.snap;
|
||||
materialUniforms.uAffine.value = retroState.affine;
|
||||
}
|
||||
|
||||
// Inject PS1 vertex behaviour into any built-in material (Lambert recommended).
|
||||
export function patchRetroMaterial(material) {
|
||||
material.onBeforeCompile = (shader) => {
|
||||
shader.uniforms.uResolution = materialUniforms.uResolution;
|
||||
shader.uniforms.uSnap = materialUniforms.uSnap;
|
||||
shader.uniforms.uAffine = materialUniforms.uAffine;
|
||||
|
||||
shader.vertexShader = shader.vertexShader
|
||||
.replace('#include <common>', `#include <common>
|
||||
uniform vec2 uResolution;
|
||||
uniform float uSnap;
|
||||
uniform float uAffine;
|
||||
varying float vAffineW;`)
|
||||
.replace('#include <project_vertex>', `#include <project_vertex>
|
||||
{
|
||||
// PS1 vertex snapping: quantize NDC position to the internal pixel grid.
|
||||
vec2 grid = uResolution * 0.5;
|
||||
vec2 ndc = gl_Position.xy / gl_Position.w;
|
||||
vec2 snapped = floor(ndc * grid + 0.5) / grid;
|
||||
gl_Position.xy = mix(ndc, snapped, uSnap) * gl_Position.w;
|
||||
// Affine texturing: pre-multiply UV by w so the GPU's perspective
|
||||
// correction cancels out, then divide by interpolated w in the fragment.
|
||||
vAffineW = gl_Position.w;
|
||||
#ifdef USE_MAP
|
||||
vMapUv = mix(vMapUv, vMapUv * gl_Position.w, uAffine);
|
||||
#endif
|
||||
}`);
|
||||
|
||||
shader.fragmentShader = shader.fragmentShader
|
||||
.replace('#include <common>', `#include <common>
|
||||
uniform float uAffine;
|
||||
varying float vAffineW;`)
|
||||
.replace('#include <map_fragment>', `
|
||||
#ifdef USE_MAP
|
||||
vec2 retroUv = mix(vMapUv, vMapUv / vAffineW, uAffine);
|
||||
vec4 sampledDiffuseColor = texture2D(map, retroUv);
|
||||
diffuseColor *= sampledDiffuseColor;
|
||||
#endif`);
|
||||
};
|
||||
material.needsUpdate = true;
|
||||
return material;
|
||||
}
|
||||
|
||||
// Swap a GLTF's PBR materials for gouraud-ish Lambert + retro patch, force
|
||||
// nearest-filtered textures (no mipmaps — PS1 had none).
|
||||
export function retrofyObject(root) {
|
||||
root.traverse((obj) => {
|
||||
if (!obj.isMesh) return;
|
||||
const src = obj.material;
|
||||
const mats = Array.isArray(src) ? src : [src];
|
||||
const out = mats.map((m) => {
|
||||
const lam = new THREE.MeshLambertMaterial({
|
||||
map: m.map || null,
|
||||
color: m.color ? m.color.clone() : new THREE.Color(0xffffff),
|
||||
transparent: m.transparent || false,
|
||||
opacity: m.opacity !== undefined ? m.opacity : 1,
|
||||
side: m.side !== undefined ? m.side : THREE.FrontSide,
|
||||
});
|
||||
if (lam.map) {
|
||||
lam.map.magFilter = THREE.NearestFilter;
|
||||
lam.map.minFilter = THREE.NearestFilter;
|
||||
lam.map.generateMipmaps = false;
|
||||
lam.map.needsUpdate = true;
|
||||
}
|
||||
return patchRetroMaterial(lam);
|
||||
});
|
||||
obj.material = Array.isArray(src) ? out : out[0];
|
||||
});
|
||||
return root;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// RetroScreen: low-res framebuffer + upscale pass (crush/dither/scanlines)
|
||||
// ---------------------------------------------------------------------------
|
||||
const POST_FRAG = `
|
||||
precision highp float;
|
||||
varying vec2 vUv;
|
||||
uniform sampler2D tFrame;
|
||||
uniform vec2 uResolution;
|
||||
uniform float uLevels;
|
||||
uniform float uDither;
|
||||
uniform float uScanlines;
|
||||
|
||||
float bayer4(vec2 p) {
|
||||
const mat4 B = mat4(
|
||||
0.0, 8.0, 2.0, 10.0,
|
||||
12.0, 4.0, 14.0, 6.0,
|
||||
3.0, 11.0, 1.0, 9.0,
|
||||
15.0, 7.0, 13.0, 5.0) / 16.0;
|
||||
ivec2 i = ivec2(mod(p, 4.0));
|
||||
return B[i.x][i.y];
|
||||
}
|
||||
|
||||
void main() {
|
||||
vec3 c = texture2D(tFrame, vUv).rgb;
|
||||
// Bayer dither in internal-pixel space, then crush to uLevels per channel.
|
||||
vec2 ip = floor(vUv * uResolution);
|
||||
float d = (bayer4(ip) - 0.5) * (uDither / uLevels);
|
||||
c = floor((c + d) * uLevels + 0.5) / uLevels;
|
||||
// CRT scanlines on output lines.
|
||||
float scan = 1.0 - uScanlines * 0.25 * mod(floor(gl_FragCoord.y), 2.0);
|
||||
gl_FragColor = vec4(c * scan, 1.0);
|
||||
}`;
|
||||
|
||||
const POST_VERT = `
|
||||
varying vec2 vUv;
|
||||
void main() { vUv = uv; gl_Position = vec4(position.xy, 0.0, 1.0); }`;
|
||||
|
||||
export class RetroScreen {
|
||||
constructor(renderer) {
|
||||
this.renderer = renderer;
|
||||
this.target = null;
|
||||
this._makeTarget();
|
||||
this.postScene = new THREE.Scene();
|
||||
this.postCam = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
|
||||
this.postMat = new THREE.ShaderMaterial({
|
||||
vertexShader: POST_VERT,
|
||||
fragmentShader: POST_FRAG,
|
||||
uniforms: {
|
||||
tFrame: { value: null },
|
||||
uResolution: { value: retroState.resolution },
|
||||
uLevels: { value: retroState.colorLevels },
|
||||
uDither: { value: retroState.dither },
|
||||
uScanlines: { value: retroState.scanlines },
|
||||
},
|
||||
depthTest: false,
|
||||
depthWrite: false,
|
||||
});
|
||||
this.postScene.add(new THREE.Mesh(new THREE.PlaneGeometry(2, 2), this.postMat));
|
||||
}
|
||||
|
||||
_makeTarget() {
|
||||
if (this.target) this.target.dispose();
|
||||
const r = retroState.resolution;
|
||||
this.target = new THREE.WebGLRenderTarget(r.x, r.y, {
|
||||
magFilter: retroState.pixelate ? THREE.NearestFilter : THREE.LinearFilter,
|
||||
minFilter: retroState.pixelate ? THREE.NearestFilter : THREE.LinearFilter,
|
||||
depthBuffer: true,
|
||||
});
|
||||
}
|
||||
|
||||
setResolution(w, h) {
|
||||
retroState.resolution.set(w, h);
|
||||
this._makeTarget();
|
||||
}
|
||||
|
||||
render(scene, camera, retroOn) {
|
||||
updateRetroUniforms();
|
||||
if (!retroOn) {
|
||||
this.renderer.setRenderTarget(null);
|
||||
this.renderer.render(scene, camera);
|
||||
return;
|
||||
}
|
||||
this.postMat.uniforms.uLevels.value = retroState.colorLevels;
|
||||
this.postMat.uniforms.uDither.value = retroState.dither;
|
||||
this.postMat.uniforms.uScanlines.value = retroState.scanlines;
|
||||
this.renderer.setRenderTarget(this.target);
|
||||
this.renderer.render(scene, camera);
|
||||
this.postMat.uniforms.tFrame.value = this.target.texture;
|
||||
this.renderer.setRenderTarget(null);
|
||||
this.renderer.render(this.postScene, this.postCam);
|
||||
}
|
||||
}
|
||||
1373
vendor/BufferGeometryUtils.js
vendored
Normal file
1373
vendor/BufferGeometryUtils.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
4727
vendor/GLTFLoader.js
vendored
Normal file
4727
vendor/GLTFLoader.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1523
vendor/OrbitControls.js
vendored
Normal file
1523
vendor/OrbitControls.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
54571
vendor/three.module.js
vendored
Normal file
54571
vendor/three.module.js
vendored
Normal file
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue
Block a user