From 8f48237a8a90a3e763b0291f6be775f59f689bd6 Mon Sep 17 00:00:00 2001 From: Xeltalliv <90340988+Xeltalliv@users.noreply.github.com> Date: Sat, 20 Jul 2024 10:01:51 +0300 Subject: [PATCH] Xeltalliv/simple3D: Feature update v1.1.0 (#1618) --- docs/Xeltalliv/simple3D.md | 64 +++++++- extensions/Xeltalliv/simple3D.js | 253 ++++++++++++++++++++++++++----- 2 files changed, 271 insertions(+), 46 deletions(-) diff --git a/docs/Xeltalliv/simple3D.md b/docs/Xeltalliv/simple3D.md index 3ac5c8ee4c..2f96c0b1e3 100644 --- a/docs/Xeltalliv/simple3D.md +++ b/docs/Xeltalliv/simple3D.md @@ -177,7 +177,7 @@ draw to the screen at X,Y. Use Z for depth check. And now it's time to make some actual Simple3D scratch blocks code. In Simple3D those transformations are combined into 1 big transformation, and all the steps have to be specified in reverse. ```scratch -start with perspective FOV (90) near (0.01) far (1000) :: sensing +start with perspective FOV (90) near (0.1) far (1000) :: sensing rotate around [X v] by ((0) - (camRotX)) degrees :: sensing rotate around [Y v] by ((0) - (camRotY)) degrees :: sensing move X ((0) - (camX)) Y ((0) - (camY)) Z ((0) - (camZ)) :: sensing @@ -190,7 +190,7 @@ draw mesh [my mesh] :: sensing Doing all of those steps again for every mesh you want to draw is inefficient. This is where doing steps in reverse becomes helpful. Combined with wrapper block, which saves the transformation when entering it, and restores it when exiting it, it is possible to do this: ```scratch -start with perspective FOV (90) near (0.01) far (1000) :: sensing +start with perspective FOV (90) near (0.1) far (1000) :: sensing rotate around [X v] by ((0) - (camRotX)) degrees :: sensing rotate around [Y v] by ((0) - (camRotY)) degrees :: sensing move X ((0) - (camX)) Y ((0) - (camY)) Z ((0) - (camZ)) :: sensing @@ -209,7 +209,7 @@ This will work and it is efficient, however this extension also provides advance And when you create one large transformation by youself, it has no way of doing that. Which is why, currently the correct way to setup transformations is like this: ```scratch configure [to projected from view space v] transformation :: sensing -start with perspective FOV (90) near (0.01) far (1000) :: sensing +start with perspective FOV (90) near (0.1) far (1000) :: sensing configure [to view space from world space v] transformation :: sensing start with no transformation :: sensing @@ -456,6 +456,22 @@ Transforms on how to get from original to current will be calculated and applied Supplied list of transforms must have length divisible by 16. If not, operation fails. When setting one of the transforms, while the other one is not defined or has different length, the one being set will be set to both. +--- +```scratch +set [my mesh] interleaved [XY positions v] [list v] :: sensing +``` + +Used for setting vertex data. Does the same as those blocks: +```scratch +set [my mesh] positions XY [listX v] [listY v] :: sensing +set [my mesh] positions XYZ [listX v] [listY v] [listZ v] :: sensing +set [my mesh] colors RGB [listR v] [listG v] [listB v] :: sensing +set [my mesh] colors RGBA [listR v] [listG v] [listB v] [listA v] :: sensing +set [my mesh] texture coordinates UV [listU v] [listV v] :: sensing +set [my mesh] texture coordinates UVW [listU v] [listV v] [listW v] :: sensing +``` +but from a single list with all the components interleaved. + --- ```scratch set [my mesh] instance [transfroms v] [list v] :: sensing @@ -597,11 +613,49 @@ Default for mesh is "off". ```scratch set [my mesh] accurate interpolation (on v) :: sensing ``` +(DEPRECTATED) Used for enabling a more accurate interpolation method which doesn't have issues of texture coordinates extrapolating outside of the specified range on the triangle edges, causing unpleasant looking seams. It is more computationally expensive and should only be used when that is an issue. Enabling mipmapping and/or anisatropic filtering may prevent it from working and reintroduce seams. Default for mesh is "off". +--- +```scratch +set [my mesh] compute color (once at pixel center v) :: sensing +``` +Replaces the deprectated "accurate interpolation" block. + +Changes how color of each pixel is computed when MSAA antialiasing is enabled. + +Sometimes it can be beneficial for visulas to make edges of rendered 3D graphics smoothed out instead of having sharply transitioning pixel colors. That is the problem that different antialiasing techniques are trying to slove. For now, in Simple3D extension MSAA antialiasing is always enabled for the main Simple3D layer, and always disabled when rendering to textures. + +The simplest way to do antialiasing is called [Supersampling](https://en.wikipedia.org/wiki/Supersampling) and consists of rendering the image at higher resoultion then what is needed and then downscaling it to lower resolution by averageing colors. It works, but it is quite slow. + +A cheaper alternative to supersampling is a technique known as [Multi-sample Antialiasing (MSAA)](https://en.wikipedia.org/wiki/Multisample_anti-aliasing). It still consists of rendering the image at higher resolution by giving each pixel multiple sub-pixels, however, the color for all the sub-pixels of a pixel is only computed once, usually based on position in the center of the pixel. At the end, the colors of all of the sub-pixels get averaged and the result is a rendered image with smooth edges. Sub-pixels are often referred as samples. + +Unlike supersampling, MSAA only smoothes out primitive edges and not the sharp pixelated transitions on the primitive itself (e.g. textures). + +- `once at pixel center` + + This is a typical MSAA as described above. + + It has an issue where if some of the samples on the edge of a pixel fall within the drawn primitive, but the center of a pixel doesn't, then the color will still be computed for the center of the pixel, causing passed in UV coordinates and vertex colors to be extrapolated beyond the specified range. It often results in visible texture seams casued by adjacent texture data bleeding into pixels that shouldn't have it or incorrect colors on edges. + + Though, for most use cases this option is good enough with issue not being noticable. Since this is computationally the cheapest option, it is default. +- `once at midpoint of covered samples` + + This solves the issue described above by still computing color once, but instead of always doing it in the center of the pixel, which may not always fall within the primitive, it does it at the midpoint of all the samples that passed the inside-of-primitive check. Since all primitives are convex, this midpoint is also guaranteed to be within the primitive. This option is more computationally expensive, and as such, disabled by default. + +- `separately for each sample` + + Computes color separately at each sample, turning this into Supersampling. This option relies on OES_shader_multisample_interpolation and as such isn't supported everywhere. It is also the most computationally expensive option. + +Note that enabling mipmapping and/or anisatropic filtering may reintroduce seams regardless of what was selected with this block. + +Using `separately for each sample` with fallback to `once at midpoint of covered samples` can be implemented by calling the block twice. Selecting `separately for each sample` when it isn't supported will do nothing and keep the previous value. + +Default for mesh is "once at pixel center". + --- ```scratch set [my mesh] vertex draw range from (1) to (6) :: sensing @@ -715,7 +769,7 @@ Transformation `custom` does not affect anything. Use it for your own calculatio --- ```scratch -start with perspective FOV (90) near (0.01) far (1000) :: sensing +start with perspective FOV (90) near (0.1) far (1000) :: sensing ``` Overwrites currently active transformation with the viewspace to clipspace conversion transformation for perspective projection. **Camera is assumed to be facing negative Z.** @@ -734,7 +788,7 @@ when resolution changes :: sensing hat --- ```scratch -start with orthographic near (0.01) far (1000) :: sensing +start with orthographic near (0.1) far (1000) :: sensing ``` Overwrites currently active transformation with the viewspace to clipspace conversion transformation for orthographic projection. **Camera is assumed to be facing negative Z.** diff --git a/extensions/Xeltalliv/simple3D.js b/extensions/Xeltalliv/simple3D.js index 9540ffb667..cc465f1983 100644 --- a/extensions/Xeltalliv/simple3D.js +++ b/extensions/Xeltalliv/simple3D.js @@ -3,7 +3,7 @@ // Description: Make GPU accelerated 3D projects easily. // By: Vadik1 // License: MPL-2.0 AND BSD-3-Clause -// Version: 1.0.4 +// Version: 1.1.0 (function (Scratch) { "use strict"; @@ -11,7 +11,8 @@ /* * A modified version of m4 library based on one of the earlier lessons on webglfundamentals.org * All lessons can be found on https://github.com/gfxfundamentals/webgl-fundamentals/tree/master - * licensed under BSD 3-Clause license + * licensed under BSD 3-Clause license. + * Only this section of the code is BSD 3-Clause. The rest of the extension is MPL-2.0. */ /* @@ -901,6 +902,9 @@ handle(output) { this.resolveFn(output.data); } + destroy() { + if (this.worker) this.worker.terminate(); + } } class SimpleSkin extends Scratch.vm.renderer.exports.Skin { constructor(id, renderer) { @@ -914,16 +918,16 @@ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); this._texture = texture; this._nativeSize = renderer.getNativeSize(); + this._boundOnNativeSizeChanged = this.onNativeSizeChanged.bind(this); this._rotationCenter = [this._nativeSize[0] / 2, this._nativeSize[1] / 2]; - renderer.on("NativeSizeChanged", this.onNativeSizeChanged.bind(this)); - const urq = renderer._updateRenderQuality.bind(renderer); - renderer._updateRenderQuality = (...args) => { - urq(args); - this.resizeCanvas(); - }; + renderer.on("NativeSizeChanged", this._boundOnNativeSizeChanged); this.resizeCanvas(); } dispose() { + renderer.removeListener( + "NativeSizeChanged", + this._boundOnNativeSizeChanged + ); if (this._texture) { this._renderer.gl.deleteTexture(this._texture); this._texture = null; @@ -985,33 +989,67 @@ } // Create drawable and skin - const skinId = renderer._nextSkinId++; + skinId = renderer._nextSkinId++; const skin = new SimpleSkin(skinId, renderer); renderer._allSkins[skinId] = skin; - const drawableId = renderer.createDrawable("simple3D"); + drawableId = renderer.createDrawable("simple3D"); + const drawable = renderer._allDrawables[drawableId]; renderer.updateDrawableSkinId(drawableId, skinId); - redraw(); - const drawOriginal = renderer.draw; - renderer.draw = function () { - if (this.dirty) redraw(); - drawOriginal.call(this); + // Detect resizing + drawable.setHighQuality = function (...args) { + Object.getPrototypeOf(this).setHighQuality(...args); + this.skin.resizeCanvas(); }; - function redraw() { + // Support for SharkPool's Layer Control extension + drawable.customDrawableName = "Simple3D Layer"; + + if (!publicApi.redraw) { + const drawOriginal = renderer.draw; + renderer.draw = function () { + if (this.dirty && publicApi.redraw) publicApi.redraw(); + drawOriginal.call(this); + }; + } + + publicApi.redraw = function () { skin.updateContent(canvas); runtime.requestRedraw(); - } + }; + publicApi.redraw(); + } + function removeSimple3DLayer() { + renderer.destroyDrawable(drawableId, "simple3D"); + renderer.destroySkin(skinId); - publicApi.redraw = redraw; + const index = renderer._groupOrdering.indexOf("simple3D"); + if (index == -1) return; + const start = renderer._layerGroups["simple3D"].drawListOffset; + const end = + renderer._layerGroups[renderer._groupOrdering[index + 1]].drawListOffset; + if (start !== end) return; + renderer._groupOrdering.splice(index, 1); + delete renderer._layerGroups["simple3D"]; + for (let i = 0; i < renderer._groupOrdering.length; i++) { + renderer._layerGroups[renderer._groupOrdering[i]].groupIndex = i; + } + publicApi.redraw = null; } const vshSrc = ` -precision highp float; - +#ifdef MSAA_CENTROID +#define INTERPOLATION centroid +#endif +#ifdef MSAA_SAMPLE +#extension GL_OES_shader_multisample_interpolation : require +#define INTERPOLATION sample +#endif #ifndef INTERPOLATION #define INTERPOLATION #endif +precision highp float; + in vec4 a_position; #ifdef COLORS in vec4 a_color; @@ -1172,13 +1210,20 @@ void main() { } `; const fshSrc = ` -precision mediump float; - +#ifdef MSAA_CENTROID +#define INTERPOLATION centroid +#endif +#ifdef MSAA_SAMPLE +#extension GL_OES_shader_multisample_interpolation : require +#define INTERPOLATION sample +#endif #ifndef INTERPOLATION #define INTERPOLATION #endif -centroid in vec4 v_color; +precision mediump float; + +INTERPOLATION in vec4 v_color; #ifdef TEXTURES #if TEXTURES == 2 INTERPOLATION in vec2 v_uv; @@ -1465,6 +1510,7 @@ void main() { target = gl.ARRAY_BUFFER ) { if (!mesh || !value) return; + if (value.length % size !== 0) return; if (mesh.uploadOffset < 0) { const buffer = mesh.myBuffers[name] ?? (mesh.myBuffers[name] = new Buffer(type)); @@ -1502,8 +1548,8 @@ void main() { const runtime = vm.runtime; const extensionId = "xeltallivSimple3D"; - const canvas = document.createElement("canvas"); - const gl = canvas.getContext("webgl2"); + let canvas = document.createElement("canvas"); + let gl = canvas.getContext("webgl2"); if (!gl) alert( "Simple 3D extension failed to get WebGL2 context. If it worked before, try restarting your browser or rebooting your device. If not, your GPU might not support WebGL2" @@ -1512,12 +1558,14 @@ void main() { gl.getExtension("EXT_texture_filter_anisotropic") || gl.getExtension("MOZ_EXT_texture_filter_anisotropic") || gl.getExtension("WEBKIT_EXT_texture_filter_anisotropic"); + const ext_smi = gl.getExtension("OES_shader_multisample_interpolation"); gl.enable(gl.DEPTH_TEST); gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true); // prettier-ignore const Blendings = { "overwrite color (fastest for opaque)": [false], "default": [true, gl.ONE, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA, gl.FUNC_ADD], + "default behind": [true, gl.ONE_MINUS_DST_ALPHA, gl.ONE, gl.ONE_MINUS_DST_ALPHA, gl.ONE, gl.FUNC_ADD], "additive": [true, gl.ONE, gl.ONE, gl.ZERO, gl.ONE, gl.FUNC_ADD], "subtractive": [true, gl.ONE, gl.ONE, gl.ZERO, gl.ONE, gl.FUNC_REVERSE_SUBTRACT], "multiply": [true, gl.DST_COLOR, gl.ONE_MINUS_SRC_ALPHA, gl.DST_COLOR, gl.ONE_MINUS_SRC_ALPHA, gl.FUNC_ADD], @@ -1569,6 +1617,9 @@ void main() { publicApi.externalTransforms ?? (publicApi.externalTransforms = {}); const canvasRenderTarget = new CanvasRenderTarget(); + let drawableId = null; + let skinId = null; + let currentRenderTarget; let transforms; let transformed; @@ -1713,11 +1764,12 @@ void main() { }, }, def: function ({ RED, GREEN, BLUE, ALPHA }) { + const alpha = Cast.toNumber(ALPHA); gl.clearColor( - Cast.toNumber(RED), - Cast.toNumber(GREEN), - Cast.toNumber(BLUE), - Cast.toNumber(ALPHA) + Cast.toNumber(RED) * alpha, + Cast.toNumber(GREEN) * alpha, + Cast.toNumber(BLUE) * alpha, + alpha ); }, }, @@ -2310,6 +2362,62 @@ void main() { mesh.update(); }, }, + { + opcode: "setMeshInterleaved", + blockType: BlockType.COMMAND, + text: "set [NAME] interleaved [PROPERTY] [SRCLIST]", + arguments: { + NAME: { + type: ArgumentType.STRING, + defaultValue: "my mesh", + }, + PROPERTY: { + type: ArgumentType.STRING, + menu: "interleavedProperty", + }, + SRCLIST: { + type: ArgumentType.STRING, + menu: "lists", + }, + }, + def: function ({ NAME, PROPERTY, SRCLIST }, { target }) { + let bufferName, size, type; + if (PROPERTY == "XY positions") { + bufferName = "position"; + size = 2; + type = Float32Array; + } + if (PROPERTY == "XYZ positions") { + bufferName = "position"; + size = 3; + type = Float32Array; + } + if (PROPERTY == "RGB colors") { + bufferName = "colors"; + size = 3; + type = Uint8Array; + } + if (PROPERTY == "RGBA colors") { + bufferName = "colors"; + size = 4; + type = Uint8Array; + } + if (PROPERTY == "UV texture coordinates") { + bufferName = "texCoords"; + size = 2; + type = Float32Array; + } + if (PROPERTY == "UVW texture coordinates") { + bufferName = "texCoords"; + size = 3; + type = Float32Array; + } + if (!bufferName) return; + const mesh = meshes.get(Cast.toString(NAME)); + const value = compact(target, [SRCLIST], type); + uploadBuffer(mesh, bufferName, value, size, 0); + }, + }, { opcode: "setMeshInstances", blockType: BlockType.COMMAND, @@ -2588,6 +2696,7 @@ void main() { opcode: "setMeshCentroidInterpolation", blockType: BlockType.COMMAND, text: "set [NAME] accurate interpolation [USECENTROID]", + hideFromPalette: true, arguments: { NAME: { type: ArgumentType.STRING, @@ -2602,7 +2711,32 @@ void main() { const mesh = meshes.get(Cast.toString(NAME)); const useCentroid = Cast.toBoolean(USECENTROID); if (!mesh) return; - mesh.myData.useCentroidInterpolation = useCentroid; + mesh.myData.interpolation = useCentroid ? "MSAA_CENTROID" : ""; + mesh.update(); + }, + }, + { + opcode: "setMeshMultiSampleInterpolation", + blockType: BlockType.COMMAND, + text: "set [NAME] compute color [MODE]", + arguments: { + NAME: { + type: ArgumentType.STRING, + defaultValue: "my mesh", + }, + MODE: { + type: ArgumentType.STRING, + menu: "multiSampleInterpolation", + }, + }, + def: function ({ NAME, MODE }, { target }) { + const mesh = meshes.get(Cast.toString(NAME)); + if (!mesh) return; + if (MODE === "once at pixel center") mesh.myData.interpolation = ""; + if (MODE === "once at midpoint of covered samples") + mesh.myData.interpolation = "MSAA_CENTROID"; + if (MODE === "separately for each sample" && ext_smi) + mesh.myData.interpolation = "MSAA_SAMPLE"; mesh.update(); }, }, @@ -2705,8 +2839,7 @@ void main() { flags.push(`SKINNING ${mesh.buffers.boneIndices.size}`); flags.push(`BONE_COUNT ${mesh.bonesDiff.length / 16}`); } - if (mesh.data.useCentroidInterpolation) - flags.push("INTERPOLATION centroid"); + if (mesh.data.interpolation) flags.push(mesh.data.interpolation); if (mesh.data.alphaTest > 0) flags.push("ALPHATEST"); if (mesh.data.makeOpaque) flags.push("MAKE_OPAQUE"); if (mesh.data.billboarding) flags.push("BILLBOARD"); @@ -3217,6 +3350,7 @@ void main() { COLOR = Cast.toRgbColorObject(COLOR); BORDERSIZE = Cast.toNumber(BORDERSIZE); BORDERCOLOR = Cast.toRgbColorObject(BORDERCOLOR); + const BORDERSIZECEIL = Math.ceil(BORDERSIZE); imageSourceSync = null; imageSource = new Promise((resolve, reject) => { const canv = document.createElement("canvas"); @@ -3224,9 +3358,13 @@ void main() { ctx.font = FONT; const m = ctx.measureText(TEXT); canv.width = - m.actualBoundingBoxLeft + m.actualBoundingBoxRight + 2 * BORDERSIZE; + m.actualBoundingBoxLeft + + m.actualBoundingBoxRight + + 2 * BORDERSIZECEIL; canv.height = - m.fontBoundingBoxAscent + m.fontBoundingBoxDescent + 2 * BORDERSIZE; + m.fontBoundingBoxAscent + + m.fontBoundingBoxDescent + + 2 * BORDERSIZECEIL; ctx.clearRect(0, 0, canv.width, canv.height); ctx.font = FONT; ctx.lineWidth = BORDERSIZE; @@ -3234,13 +3372,13 @@ void main() { ctx.strokeStyle = `rgba(${BORDERCOLOR.r},${BORDERCOLOR.g},${BORDERCOLOR.b},${(BORDERCOLOR.a ?? 255) / 255})`; ctx.fillText( TEXT, - m.actualBoundingBoxLeft + BORDERSIZE, - m.fontBoundingBoxAscent + BORDERSIZE + m.actualBoundingBoxLeft + BORDERSIZECEIL, + m.fontBoundingBoxAscent + BORDERSIZECEIL ); ctx.strokeText( TEXT, - m.actualBoundingBoxLeft + BORDERSIZE, - m.fontBoundingBoxAscent + BORDERSIZE + m.actualBoundingBoxLeft + BORDERSIZECEIL, + m.fontBoundingBoxAscent + BORDERSIZECEIL ); imageSourceSync = { width: canv.width, @@ -3412,6 +3550,7 @@ void main() { if (DIR == "down") return lastTextMeasurement.fontBoundingBoxDescent; if (DIR == "left") return lastTextMeasurement.actualBoundingBoxLeft; if (DIR == "right") return lastTextMeasurement.actualBoundingBoxRight; + if (DIR == "x step") return lastTextMeasurement.width; return 0; }, }, @@ -3471,7 +3610,7 @@ void main() { }, NEAR: { type: ArgumentType.NUMBER, - defaultValue: 0.01, + defaultValue: 0.1, }, FAR: { type: ArgumentType.NUMBER, @@ -3494,7 +3633,7 @@ void main() { arguments: { NEAR: { type: ArgumentType.NUMBER, - defaultValue: 0.01, + defaultValue: 0.1, }, FAR: { type: ArgumentType.NUMBER, @@ -4349,6 +4488,17 @@ void main() { "UV offsets and sizes", ], }, + interleavedProperty: { + acceptReporters: false, + items: [ + "XY positions", + "XYZ positions", + "RGB colors", + "RGBA colors", + "UV texture coordinates", + "UVW texture coordinates", + ], + }, renderTargetProperty: { acceptReporters: false, items: [ @@ -4373,12 +4523,20 @@ void main() { }, directions: { acceptReporters: true, - items: ["up", "down", "left", "right"], + items: ["up", "down", "left", "right", "x step"], }, bufferUsage: { acceptReporters: true, items: ["rarely", "frequently fully", "frequently partially"], }, + multiSampleInterpolation: { + acceptReporters: true, + items: [ + "once at pixel center", + "once at midpoint of covered samples", + "separately for each sample", + ], + }, }, }; @@ -4389,6 +4547,19 @@ void main() { ).hideFromPalette = Object.keys(externalTransforms).length == 0; return extInfo; } + dispose() { + resetEverything(); + removeSimple3DLayer(); + modelDecoder.destroy(); + runtime.removeListener("PROJECT_LOADED", resetEverything); + canvas = null; + gl = null; + const noop = () => {}; + for (let block of definitions) { + if (block == "---") continue; + Extension.prototype[block.opcode ?? block.func] = noop; + } + } fontsMenu() { const defaultFonts = [ "Sans Serif",