From e25b3bae1c5faf64f142f4db0889b8c74f8283e3 Mon Sep 17 00:00:00 2001 From: Zellyn Hunter Date: Sat, 28 Apr 2018 16:42:53 -0400 Subject: [PATCH] initial javascript wip --- README.md | 5 +- favicon.ico | Bin 0 -> 5558 bytes images/airheart-560x192.png | Bin 0 -> 9147 bytes index.html | 280 +++++++++++++++++++++ screenEmu.js | 405 ++++++++++++++++++++++++++++++ textures/Shadow Mask Aperture.png | Bin 0 -> 253 bytes textures/Shadow Mask Bayer.png | Bin 0 -> 2385 bytes textures/Shadow Mask Inline.png | Bin 0 -> 885 bytes textures/Shadow Mask LCD.png | Bin 0 -> 581 bytes textures/Shadow Mask Triad.png | Bin 0 -> 2038 bytes 10 files changed, 689 insertions(+), 1 deletion(-) create mode 100644 favicon.ico create mode 100644 images/airheart-560x192.png create mode 100644 index.html create mode 100644 screenEmu.js create mode 100644 textures/Shadow Mask Aperture.png create mode 100644 textures/Shadow Mask Bayer.png create mode 100644 textures/Shadow Mask Inline.png create mode 100644 textures/Shadow Mask LCD.png create mode 100644 textures/Shadow Mask Triad.png diff --git a/README.md b/README.md index 4f06d40..318863f 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,5 @@ # apple2shader -Port(s) of the OpenEmulator NTSC/PAL GPU shader +Port(s) of the OpenEmulator NTSC/PAL GPU shader. + +This is *very much* a work in progress. Right now I'm putting it on +github simply for backup. diff --git a/favicon.ico b/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..626f862ad76807f4ac8ba17c85875ae0d2000238 GIT binary patch literal 5558 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 + + + + MDN Games: Shaders demo + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Decoder + +
Brightness
Contrast
Saturation
Hue
White Only
Horizontal Center
Horizontal Size
Vertical Center
Vertical Size
Luma Bandwidth
Chroma Bandwidth
B/W Bandwidth
Barrel
Scanline Level
Shadow Mask Level
Shadow Mask Dot Pitch
Shadow Mask + +
Persistence
Center Lighting
Luminance Gain
+
+
+ + +
+
+ + + diff --git a/screenEmu.js b/screenEmu.js new file mode 100644 index 0000000..6cad81a --- /dev/null +++ b/screenEmu.js @@ -0,0 +1,405 @@ +"use strict"; + +let screenEmu = (function () { + // From AppleIIVideo.cpp + + let HORIZ_START = 16; + let HORIZ_BLANK = (9 + HORIZ_START) // 25; + let HORIZ_DISPLAY = 40; + let HORIZ_TOTAL = (HORIZ_BLANK + HORIZ_DISPLAY) // 65; + + let CELL_WIDTH = 14; + let CELL_HEIGHT = 8; + + let VERT_NTSC_START = 38; + let VERT_PAL_START = 48; + let VERT_DISPLAY = 192; + + let BLOCK_WIDTH = HORIZ_DISPLAY; // 40 + let BLOCK_HEIGHT = (VERT_DISPLAY / CELL_HEIGHT); // 24 + + // From CanvasInterface.h + + let NTSC_FSC = 315/88 * 1e6; // 3579545 = 3.5 Mhz: Color Subcarrier + let NTSC_4FSC = 4 * NTSC_FSC; // 14318180 = 14.3 Mhz + let NTSC_HTOTAL = (63+5/9) * 1e-6; + let NTSC_HLENGTH = (52+8/9) * 1e-6; + let NTSC_HHALF = (35+2/3) * 1e-6; + let NTSC_HSTART = NTSC_HHALF - NTSC_HLENGTH/2; + let NTSC_HEND = NTSC_HHALF + NTSC_HLENGTH/2; + let NTSC_VTOTAL = 262; + let NTSC_VLENGTH = 240; + let NTSC_VSTART = 19; + let NTSC_VEND = NTSC_VSTART + NTSC_VLENGTH; + + let PAL_FSC = 4433618.75; // Color subcarrier + let PAL_4FSC = 4 * PAL_FSC; + let PAL_HTOTAL = 64e-6; + let PAL_HLENGTH = 52e-6; + let PAL_HHALF = (37+10/27) * 1e-6; + let PAL_HSTART = PAL_HHALF - PAL_HLENGTH / 2; + let PAL_HEND = PAL_HHALF + PAL_HLENGTH / 2; + let PAL_VTOTAL = 312; + let PAL_VLENGTH = 288; + let PAL_VSTART = 21; + let PAL_VEND = PAL_VSTART + PAL_VLENGTH; + + // From AppleIIVideo::updateTiming + let ntscClockFrequency = NTSC_4FSC * HORIZ_TOTAL / 912; + let ntscVisibleRect = [[ntscClockFrequency * NTSC_HSTART, NTSC_VSTART], + [ntscClockFrequency * NTSC_HLENGTH, NTSC_VLENGTH]]; + let ntscDisplayRect = [[HORIZ_START, VERT_NTSC_START], + [HORIZ_DISPLAY, VERT_DISPLAY]]; + let ntscVertTotal = NTSC_VTOTAL; + + let palClockFrequency = 14250450.0 * HORIZ_TOTAL / 912; + let palVisibleRect = [[palClockFrequency * PAL_HSTART, PAL_VSTART], + [palClockFrequency * PAL_HLENGTH, PAL_VLENGTH]]; + let palDisplayRect = [[HORIZ_START, VERT_PAL_START], + [HORIZ_DISPLAY, VERT_DISPLAY]]; + let palVertTotal = PAL_VTOTAL; + + const COMPOSITE_SHADER = ` +uniform sampler2D texture; +uniform vec2 textureSize; +uniform float subcarrier; +uniform sampler1D phaseInfo; +uniform vec3 c0, c1, c2, c3, c4, c5, c6, c7, c8; +uniform mat3 decoderMatrix; +uniform vec3 decoderOffset; + +float PI = 3.14159265358979323846264; + +vec3 pixel(in vec2 q) +{ + vec3 c = texture2D(texture, q).rgb; + vec2 p = texture1D(phaseInfo, q.y).rg; + float phase = 2.0 * PI * (subcarrier * textureSize.x * q.x + p.x); + return c * vec3(1.0, sin(phase), (1.0 - 2.0 * p.y) * cos(phase)); +} + +vec3 pixels(vec2 q, float i) +{ + return pixel(vec2(q.x + i, q.y)) + pixel(vec2(q.x - i, q.y)); +} + +void main(void) +{ + vec2 q = gl_TexCoord[0].st; + vec3 c = pixel(q) * c0; + c += pixels(q, 1.0 / textureSize.x) * c1; + c += pixels(q, 2.0 / textureSize.x) * c2; + c += pixels(q, 3.0 / textureSize.x) * c3; + c += pixels(q, 4.0 / textureSize.x) * c4; + c += pixels(q, 5.0 / textureSize.x) * c5; + c += pixels(q, 6.0 / textureSize.x) * c6; + c += pixels(q, 7.0 / textureSize.x) * c7; + c += pixels(q, 8.0 / textureSize.x) * c8; + gl_FragColor = vec4(decoderMatrix * c + decoderOffset, 1.0); +} +`; + + const DISPLAY_SHADER = ` +uniform sampler2D texture; +uniform vec2 textureSize; +uniform float barrel; +uniform vec2 barrelSize; +uniform float scanlineLevel; +uniform sampler2D shadowMask; +uniform vec2 shadowMaskSize; +uniform float shadowMaskLevel; +uniform float centerLighting; +uniform sampler2D persistence; +uniform vec2 persistenceSize; +uniform vec2 persistenceOrigin; +uniform float persistenceLevel; +uniform float luminanceGain; + +float PI = 3.14159265358979323846264; + +void main(void) +{ + vec2 qc = (gl_TexCoord[1].st - vec2(0.5, 0.5)) * barrelSize; + vec2 qb = barrel * qc * dot(qc, qc); + vec2 q = gl_TexCoord[0].st + qb; + + vec3 c = texture2D(texture, q).rgb; + + float scanline = sin(PI * textureSize.y * q.y); + c *= mix(1.0, scanline * scanline, scanlineLevel); + + vec3 mask = texture2D(shadowMask, (gl_TexCoord[1].st + qb) * shadowMaskSize).rgb; + c *= mix(vec3(1.0, 1.0, 1.0), mask, shadowMaskLevel); + + vec2 lighting = qc * centerLighting; + c *= exp(-dot(lighting, lighting)); + + c *= luminanceGain; + + vec2 qp = gl_TexCoord[1].st * persistenceSize + persistenceOrigin; + c = max(c, texture2D(persistence, qp).rgb * persistenceLevel - 0.5 / 256.0); + + gl_FragColor = vec4(c, 1.0); +} +`; + + function buildTiming(clockFrequency, displayRect, visibleRect, vertTotal) { + let vertStart = displayRect[0][1]; + // Total number of CPU cycles per frame: 17030 for NTSC. + let frameCycleNum = HORIZ_TOTAL * vertTotal; + // first displayed column. + let horizStart = Math.floor(displayRect[0][0]); + // imageSize is [14 * visible rect width in cells, visible lines] + let imageSize = [Math.floor(CELL_WIDTH * visibleRect[1][0]), + Math.floor(visibleRect[1][1])]; + // imageLeft is # of pixels from first visible point to first displayed point. + let imageLeft = Math.floor((horizStart-visibleRect[0][0]) * CELL_WIDTH); + let colorBurst = [2 * Math.PI * (-33/360 + (imageLeft % 4) / 4)]; + let cycleNum = frameCycleNum + 16; + + // First pixel that OpenEmulator draws when painting normally. + let topLeft = [imageLeft, vertStart - visibleRect[0][1]]; + // First pixel that OpenEmulator draws when painting 80-column mode. + let topLeft80Col = [imageLeft - CELL_WIDTH/2, vertStart - visibleRect[0][1]]; + + return { + clockFrequency: clockFrequency, + displayRect: displayRect, + visibleRect: visibleRect, + vertStart: vertStart, + vertTotal: vertTotal, + frameCycleNum: frameCycleNum, + horizStart: horizStart, + imageSize: imageSize, + imageLeft: imageLeft, + colorBurst: colorBurst, + cycleNum: cycleNum, + topLeft: topLeft, + topLeft80Col: topLeft80Col, + }; + } + + // https://codereview.stackexchange.com/a/128619 + const loadImage = path => + new Promise((resolve, reject) => { + const img = new Image(); + img.onload = () => resolve(img); + img.onerror = () => reject(`error loading '${path}'`); + img.src = path; + }); + + // Given an image that's 560x192, render it into the larger space + // required for NTSC or PAL. + // image: a 560x192 image, from the same domain (hence readable). + // details: NTSC_DETAILS, or PAL_DETAILS + // returns: a canvas + const screenData = (image, details) => { + if ((image.naturalWidth != 560) || (image.naturalHeight != 192)) { + throw `screenData expects an image 560x192; got ${image.naturalWidth}x${image.naturalHeight}`; + } + let canvas = document.createElement('canvas'); + let context = canvas.getContext('2d'); + let width = details.imageSize[0]; + let height = details.imageSize[1]; + canvas.width = width; + canvas.height = height; + context.fillStyle = 'rgba(0,0,0,1)'; + context.fillRect(0, 0, width, height); + context.drawImage(image, details.topLeft80Col[0], details.topLeft80Col[1]); + // let myData = context.getImageData(0, 0, image.naturalWidth, image.naturalHeight); + return canvas; + }; + + const TEXTURE_NAMES = [ + "SHADOWMASK_TRIAD", + "SHADOWMASK_INLINE", + "SHADOWMASK_APERTURE", + "SHADOWMASK_LCD", + "SHADOWMASK_BAYER", + "IMAGE_PHASEINFO", + "IMAGE_IN", + "IMAGE_DECODED", + "IMAGE_PERSISTENCE", + ]; + + const SHADER_NAMES = [ + "COMPOSITE", + "DISPLAY", + ]; + + const TextureInfo = class { + constructor(width, height, glTexture) { + this.width = width; + this.height = height; + this.glTexture = glTexture; + } + }; + + const ScreenView = class { + constructor(gl) { + this.gl = gl; + this.textures = {}; + this.shaders = {}; + } + + async initOpenGL() { + let gl = this.gl; + + gl.enable(gl.BLEND); + gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); + + this.textures = {}; + + for (let name of TEXTURE_NAMES) { + this.textures[name] = new TextureInfo(0, 0, gl.createTexture()); + } + + await this.loadTextures(); + + gl.pixelStorei(gl.PACK_ALIGNMENT, 1); + gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1); + + this.loadShaders(); + } + + freeOpenGL() { + let gl = this.gl; + + for (let name of TEXTURE_NAMES) { + gl.deleteTexture(this.textures[name].glTexture); + } + + this.deleteShaders(); + } + + loadTextures() { + return Promise.all([ + this.loadTexture("textures/Shadow Mask Triad.png", true, "SHADOWMASK_TRIAD"), + this.loadTexture("textures/Shadow Mask Inline.png", true, "SHADOWMASK_INLINE"), + this.loadTexture("textures/Shadow Mask Aperture.png", true, "SHADOWMASK_APERTURE"), + this.loadTexture("textures/Shadow Mask LCD.png", true, "SHADOWMASK_LCD"), + this.loadTexture("textures/Shadow Mask Bayer.png", true, "SHADOWMASK_BAYER"), + ]); + } + + async loadTexture(path, isMipMap, name) { + let gl = this.gl; + let textureInfo = this.textures[name]; + let image = await loadImage(path); + gl.bindTexture(gl.TEXTURE_2D, textureInfo.glTexture); + + // TODO(zellyn): implement + if (isMipMap) { + // gluBuild2DMipmaps(GL_TEXTURE_2D, GL_RGB8, + // image.getSize().width, image.getSize().height, + // getGLFormat(image.getFormat()), + // GL_UNSIGNED_BYTE, image.getPixels()); + } else { + // glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, + // image.getSize().width, image.getSize().height, + // 0, + // getGLFormat(image.getFormat()), GL_UNSIGNED_BYTE, image.getPixels()); + } + + textureInfo.width = image.naturalWidth; + textureInfo.height = image.naturalHeight; + } + + // TODO(zellyn): implement + loadShaders() { + this.loadShader("COMPOSITE", COMPOSITE_SHADER); + this.loadShader("DISPLAY", DISPLAY_SHADER); + } + + // TODO(zellyn): implement + loadShader(name, source) { + console.log(`ScreenView.loadShader(${name}): not implemented yet`); + } + + // TODO(zellyn): implement + deleteShaders() { + for (let name of SHADER_NAMES) { + if (this.shaders[name]) { + gl.deleteProgram(this.shaders[name]); + this.shaders[name] = false; + } + } + } + } + + // Resize the texture with the given name to the next + // highest power of two width and height. Wouldn't be + // necessary with webgl2. + const resizeTexture = (gl, textures, name, width, height) => { + let textureInfo = textures[name]; + if (!!textureInfo) { + throw `Cannot find texture named ${name}`; + } + if (width < 4) width = 4; + if (height < 4) height = 4; + width = 2**Math.ceil(Math.log2(width)); + height = 2**Math.ceil(Math.log2(height)); + textureInfo.width = width; + textureInfo.height = height; + gl.bindTexture(gl.TEXTURE_2D, textureInfo.glTexture); + const dummy = new Uint8Array(width * height); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, width, height, 0, gl.LUMINANCE, gl.UNSIGNED_BYTE, dummy); + }; + + const vsync = (gl) => { + // if viewport size has changed: + // glViewPort(0, 0, new_width, new_height); + + // if image updated: + // uploadImage(); + + // if configuration updated: + // configureShaders(); + + // if image or configuration updated: + // renderImage(); + + // if anything updated, or displayPersistence != 0.0 + // drawDisplayCanvas(); + }; + + // TODO(zellyn): implement + const uploadImage = (gl) => { + }; + + // TODO(zellyn): implement + const configureShaders = (gl) => { + }; + + // TODO(zellyn): implement + const renderImage = (gl) => { + }; + + // TODO(zellyn): implement + const drawDisplayCanvas = (gl) => { + }; + + return { + C: { + HORIZ_START: HORIZ_START, + HORIZ_BLANK: HORIZ_BLANK, + HORIZ_DISPLAY: HORIZ_DISPLAY, + HORIZ_TOTAL: HORIZ_TOTAL, + CELL_WIDTH: CELL_WIDTH, + CELL_HEIGHT: CELL_HEIGHT, + VERT_NTSC_START: VERT_NTSC_START, + VERT_PAL_START: VERT_PAL_START, + VERT_DISPLAY: VERT_DISPLAY, + BLOCK_WIDTH: BLOCK_WIDTH, + BLOCK_HEIGHT: BLOCK_HEIGHT, + NTSC_DETAILS: buildTiming(ntscClockFrequency, ntscDisplayRect, + ntscVisibleRect, ntscVertTotal), + PAL_DETAILS: buildTiming(palClockFrequency, palDisplayRect, + palVisibleRect, palVertTotal), + }, + loadImage: loadImage, + screenData: screenData, + resizeTexture: resizeTexture, + getScreenView: (gl) => new ScreenView(gl), + }; +})(); diff --git a/textures/Shadow Mask Aperture.png b/textures/Shadow Mask Aperture.png new file mode 100644 index 0000000000000000000000000000000000000000..4f3fb2b80fce1a11764c43131f1c993d67c6a5f2 GIT binary patch literal 253 zcmeAS@N?(olHy`uVBq!ia0vp^4M6O`!2~2@x4h5*Qj#UE5hcO-X(i=}MX3yqDfvmM z3ZA)%>8U}fi7AzZCsS>JispN|IEGZ*dUJ6jXM=-?%fX`&IVY@gPCKnRspiweVruYt z!fBT|$Cq;dpP_Ry?)$GPvsQgiF0>DM|NQq3Z@b$6k-~eWfTpm3Y!_f)U}A7!VBlb2 zU}R8W;9tvdFPB*XO>)OS@%Rizs1mID;DWoXSR@#ZScYdF(Ay&pw1mOa)z4*}Q$iB} Duen6} literal 0 HcmV?d00001 diff --git a/textures/Shadow Mask Bayer.png b/textures/Shadow Mask Bayer.png new file mode 100644 index 0000000000000000000000000000000000000000..dfc783917b3d34d31d2bdc785bc820869be8025a GIT binary patch literal 2385 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 LNkvXXu0mjf07-_f literal 0 HcmV?d00001 diff --git a/textures/Shadow Mask LCD.png b/textures/Shadow Mask LCD.png new file mode 100644 index 0000000000000000000000000000000000000000..13841865f0716ee44045d26cc6e797bb4037c1b5 GIT binary patch literal 581 zcmeAS@N?(olHy`uVBq!ia0vp^4M6O`!2~2@x4h5*Qj#UE5hcO-X(i=}MX3yqDfvmM z3ZA)%>8U}fi7AzZCsS=07#N>>x;TbZ+BbVj?A-K)Vr62>{8JS9* zZ~i^EZ}vmJX$u>>zN_ItwKI=zEtZ~m={l`s9C-B@sa-?Zl;iG25t>J_fGYIyVYv(B`84V$krO-uf{ z?(T|b`fZ=ruVOwCS6@9#K>eZ1*2e;YeBu4u>SvsftH0f%FJNChDP{eBSCIt_felP9 zj9db&8V+#IkLx#m{(Uju%q^iJZcxBk#2M6};lQ|vfwhB4M1d;+#0j(C_^X(I;q(}X z^QtF2*I+Ycf(%pHq4anEj$J(Yar^81$Lnugzt37P#a-Y2{A!>12D#lwvc6_~`?XW} z_PdvYKH;BjD+Es5mo~mRjp^t0f@cYv=I{UaOyo{$>&AC+2c~YajF|g)>F=1Ele6yk z!VzU`Mi!aG0y|4=-@Ak1FW$2$GiUtqdm@O1TaS?83{1OR$c B0{#F1 literal 0 HcmV?d00001 diff --git a/textures/Shadow Mask Triad.png b/textures/Shadow Mask Triad.png new file mode 100644 index 0000000000000000000000000000000000000000..7e03b10d5dcbe3d8c79d9e246eb28014a42d26a8 GIT binary patch literal 2038 zcmV)oHJ)`4526hW&_iKiNH{x zKmYgyxDWgS`~Z9lT!R{30*WcYTNGt5kPWl}O+YIO^+~_Aur4xN{vN#}0|A*^ZRKzxd5*(mbc6ilpvv(@@ z*sU#ANz(^qzmoy>D@d@)mi$Wv@=kVZg^})O9?P=GQi4S~^tIH@gei2aH29yeb2B#R z&VhM@)+qRelI4h(}XMhevM zgF(Z1T$4O*Tmoat>c<3Xs7erT)Ff|YxYSTX6%Pe!c#lD-0q$s$-!byn1I?Ne+z_bY zfo5tLYJh4@eskwnfh%hCZNLd*r4y{Dge%lA)Bs0Ru5%Iyx;+sLFpC~&N=zq08;|^nOaS3)f zd@9?e$+tUwsbjcW3erCZ7CK3=kZlAdxI(S0(T8{IQsjr&p&N=t1M=%5tk>&BuOo_` z`G*51CCE>*tB8@S1_=qaMBX#ENYFP#k1r05ln>D@H)5#IsbMBi=Rw~AZ1g<0Y-BHy zC8(ng9>@khqVwR&T+fEt{wKErV4wRPMF%g#=ZTyh=drI2_v?>+lwJZkOf{;pzJXpN zm6Fpnj`}(9z=FP&r|nWmqm=!&T1fDKvPKLm$HKpcX~6ryJGAV;)C6zgI(H2?1$;wS zvIRN_mb1A)88DSm%UmFfDmSN+Bfp~rKkNDOlQ;k~Q@NRaIb!Whj=W6|u8QvFeZ^>D z62a)_rGop-G*AfK7L&$)eE z6sS^2gW@p__e}HSF2p=17uA`l#EBJh(DLs7?D^!S|p_3R-%`Q{RbP@zM z_}k?<2`uC66hLqGRqf)-1e5YB9(n~Z3i#A9*{+sNMoA6*G-}gem&^T+QL1ta;iU;| zRz>yWgQ@Zf&w0^N?h(tV^G_1Xq6Td)iQL`)bk*mLLlN712DA1=h@~d6(#~@YcdJ+W zxC(pRDSPFIuG@*OlY`TvIjAlJ0EU*dNkI~^C zW9gwT@L~xyz;K|%*h5L=J77CsdW7-duZMD*4{*Ak0bt`36lspY@bF8%Y=&6$Fu{1q zTHrhV;${nb*YFc>Sa`rEPm?~+=6yq4)c|Inv=4NV9Uf}TR_5*%T{;VN8Vz8+c+|d{ zYzp~b3vVa&@Ys0W-6ZghhvKHOhnt!s&=Q_s9~l3xCjDLG{HpiMNQfB?aKYHe1u88tV8sKZ!o53u$5gdWlPV|%fva0`mH|hNQcdG>rQ0;mflSDpS zij2Uf2=rxkxx6;1wq?&c)NrVDiF1j~9PCQkh#`nCdtS5|1f^Q`8s6-&JwKSoZkTAkz2zq}Vc!OzYgZaQ9i+qn$36iH&f2|?# zq}~Qz;|XCvmj@<|%uf9Y_>qgYBF3lQx8!kwOU90$^VoWW35gkGkU<9j9sUwv0Amh0 U671H&yZ`_I07*qoM6N<$f*IhzX8-^I literal 0 HcmV?d00001