Filtro mezzitoni per le esercitazioni sugli script

Questo tutorial descrive l'implementazione di un filtro "mezzitoni" come uno script Lua per OBS. Tale filtro viene visualizzato nell'elenco dei filtri e può essere aggiunto come parte di una catena di filtri a qualsiasi sorgente video. Come effetto, il filtro imita la tecnica dei mezzitoni , utilizzata per la stampa con una serie ridotta di colori dell'inchiostro disposti in motivi. Si basa sul dithering con una trama accuratamente progettata.

Prima parte - 4 sfumature di grigio

Il tutorial è diviso in due parti. In questa prima parte creiamo uno script minimale che implementa un semplice effetto di rendering. Tale script con il suo file effetto può essere facilmente riutilizzato come punto di partenza per nuovi progetti.

Creazione di script

Analogamente al tutorial Source Shake , crea un file di script denominato filter-halftone.luacon questo contenuto:

obs = obslua

-- Returns the description displayed in the Scripts window
function script_description()
  return [[Halftone Filter
  This Lua script adds a video filter named Halftone. The filter can be added
  to a video source to reduce the number of colors of the input picture. It reproduces
  the style of a magnified printed picture.]]
end

Aggiungi il nuovo script nella finestra Script , dovrebbe apparire la descrizione (senza proprietà).

Registrazione di una prima struttura di informazioni sulla fonte

Le fonti sono supportate in Lua attraverso la source_infostruttura . Essa è definita come un normale tavolo Lua contenente una serie di chiavi obbligatorie referenziare valori e funzioni che un sottoinsieme mimico della C obs_source_infostruttura .

La source_infostruttura è registrata tramite obs_register_source. OBS utilizzerà la struttura per creare i dati interni e le impostazioni quando il filtro viene aggiunto a una sorgente video esistente. Un filtro ha il proprio insieme indipendente di proprietà e impostazioni dei dati, ovvero esiste un'istanza di tali impostazioni per ogni istanza del filtro. OBS gestisce la creazione del filtro, la distruzione e l'archiviazione persistente delle sue impostazioni.

Creiamo la struttura delle informazioni sulla sorgente per un filtro video con un insieme minimo di valori e funzioni. Di solito è registrato in script_load:

-- Called on script startup
function script_load(settings)
  obs.obs_register_source(source_info)
end

-- Definition of the global variable containing the source_info structure
source_info = {}
source_info.id = 'filter-halftone'              -- Unique string identifier of the source type
source_info.type = obs.OBS_SOURCE_TYPE_FILTER   -- INPUT or FILTER or TRANSITION
source_info.output_flags = obs.OBS_SOURCE_VIDEO -- Combination of VIDEO/AUDIO/ASYNC/etc

-- Returns the name displayed in the list of filters
source_info.get_name = function()
  return "Halftone"
end

Aggiungi il codice, aggiorna lo script, quindi scegli una fonte (qui un'immagine colorata del gioco VR Anceder) e visualizza la finestra di dialogo Filtro per la fonte (es. Filtri nel menu contestuale della fonte). Fare clic su + e il nome Mezzitoni dovrebbe apparire nell'elenco dei filtri:

filtro elenco mezzitoni

Aggiungi il nuovo filtro Mezzitoni . Per ora il filtro non ha effetto perché la source_info.createfunzione non è definita.

Compilazione e rendering di effetti

Lua non sarebbe abbastanza veloce per l'elaborazione video, quindi il filtro si basa sul calcolo della GPU. Una GPU può essere programmata tramite "shader" compilati dal driver del dispositivo grafico da GLSL per OpenGL o HLSL nel mondo MS Windows. OBS si basa su HLSL e si aspetta il codice shader nei cosiddetti file degli effetti .

Un effetto può essere compilato da un file con gs_effect_create_from_fileo da una stringa con gs_effect_createe viene distrutto con gs_effect_destroy). Come altre funzioni grafiche in OBS, la manipolazione degli effetti deve avvenire nel contesto grafico . Ciò è assicurato chiamando obs_enter_graphicse poi obs_leave_graphicsquando tutto è fatto.

La compilazione è tipicamente implementata nella source_info.createfunzione e le risorse vengono rilasciate nella source_info.destroyfunzione. source_info.createrestituisce una tabella Lua contenente i dati personalizzati utilizzati per l'istanza del filtro. La maggior parte delle funzioni della source_infostruttura fornirà questa tabella Lua passata come argomento, quindi è qui che si desidera memorizzare i dati personalizzati necessari al filtro. Si noti che se viene source_info.createrestituito nil, l'inizializzazione del filtro viene considerata non riuscita (e registrata di conseguenza).

Aggiungi questo codice allo script Lua:

-- Creates the implementation data for the source
source_info.create = function(settings, source)

  -- Initializes the custom data table
  local data = {}
  data.source = source -- Keeps a reference to this filter as a source object
  data.width = 1       -- Dummy value during initialization phase
  data.height = 1      -- Dummy value during initialization phase

  -- Compiles the effect
  obs.obs_enter_graphics()
  local effect_file_path = script_path() .. 'filter-halftone.effect.hlsl'
  data.effect = obs.gs_effect_create_from_file(effect_file_path, nil)
  obs.obs_leave_graphics()

  -- Calls the destroy function if the effect was not compiled properly
  if data.effect == nil then
    obs.blog(obs.LOG_ERROR, "Effect compilation failed for " .. effect_file_path)
    source_info.destroy(data)
    return nil
  end

  return data
end

-- Destroys and release resources linked to the custom data
source_info.destroy = function(data)
  if data.effect ~= nil then
    obs.obs_enter_graphics()
    obs.gs_effect_destroy(data.effect)
    data.effect = nil
    obs.obs_leave_graphics()
  end
end

Il file dell'effetto filter-halftone.effect.hlslè menzionato nel codice, verrà definito nella sezione successiva. Si noti che l' sourceargomento della funzione source_info.createè un riferimento all'istanza corrente del filtro come oggetto sorgente (quasi tutto è una sorgente in OBS). Questo riferimento, così come le variabili widthe height, inizializzate con valori fittizi, vengono utilizzati nella funzione di rendering.

Vale a dire, la funzione source_info.video_renderviene chiamata ogni frame per rendere l'output del filtro nel contesto grafico (non è necessario chiamare obs_enter_graphics). Per eseguire il rendering di un effetto, prima obs_source_process_filter_beginviene chiamato, quindi i parametri dell'effetto possono essere impostati e quindi obs_source_process_filter_endviene chiamato per disegnare.

Determinare la larghezza e l'altezza a cui passare obs_source_process_filter_endè in qualche modo complicato in OBS, perché il filtro è esso stesso in una catena di filtri, dove la risoluzione potrebbe teoricamente cambiare in qualsiasi fase. Il metodo usuale è recuperare la "sorgente genitore" del filtro con obs_filter_get_parent, cioè "la sorgente che viene filtrata", e quindi utilizzare le funzioni finora non documentate obs_source_get_base_widthe obs_source_get_base_height. Si noti che alcuni filtri fanno riferimento alla sorgente successiva nella catena utilizzando obs_filter_get_target, non alla sorgente filtrata (potrebbe fare la differenza a seconda del caso d'uso).

Inoltre, due funzioni aggiuntive source_info.get_widthe source_info.get_heightdevono essere definite per fornire i valori a OBS quando necessario. Le funzioni riutilizzeranno i valori determinati in source_info.video_render.

Il codice di rendering dell'effetto ha il seguente aspetto:

-- Returns the width of the source
source_info.get_width = function(data)
  return data.width
end

-- Returns the height of the source
source_info.get_height = function(data)
  return data.height
end

-- Called when rendering the source with the graphics subsystem
source_info.video_render = function(data)
  local parent = obs.obs_filter_get_parent(data.source)
  data.width = obs.obs_source_get_base_width(parent)
  data.height = obs.obs_source_get_base_height(parent)

  obs.obs_source_process_filter_begin(data.source, obs.GS_RGBA, obs.OBS_NO_DIRECT_RENDERING)

  -- Effect parameters initialization goes here

  obs.obs_source_process_filter_end(data.source, data.effect, data.width, data.height)
end

Aggiungi i blocchi di codice allo script Lua, ma non è necessario ricaricare per ora, il file dell'effetto è ancora mancante.

Semplice file di effetti di luminanza HLSL

Il codice Lua è a posto, ora crea un nuovo file chiamato filter-halftone.effect.hlslnella stessa directory dello script Lua.

: avviso: l'estensione .hlslè scelta in modo tale che un editor di codice o un IDE riconosca direttamente che si tratta di codice HLSL. Poiché il parser di OBS può perdere parentesi o parentesi sbilanciate (e non dire nulla al riguardo nei log), è molto importante avere il controllo della sintassi nell'editor.

Un file effetto segue una sintassi e una struttura rigide. È un mix di definizioni di dati statici e codice. Questi vincoli sono necessari per consentire la compilazione per la GPU e l'esecuzione massicciamente parallela. Le varie parti sono descritte di seguito.

Il nostro file di effetti inizia con le definizioni di macro che consentiranno di scrivere codice conforme a HLSL invece del dialetto degli effetti compreso da OBS, al fine di supportare un controllo completo della sintassi nell'IDE. In effetti, le parole chiave sampler_statee texture2dsono specifiche degli effetti in OBS. L'utilizzo di tali macro non è ovviamente obbligatorio (e non è così comune guardare altri file di effetti OBS):

// OBS-specific syntax adaptation to HLSL standard to avoid errors reported by the code editor
#define SamplerState sampler_state
#define Texture2D texture2d

Quindi uniformvengono definiti i due parametri obbligatori richiesti da OBS. I valori di questi parametri verranno impostati in modo trasparente da OBS in base all'immagine di input della sorgente da filtrare impostata imagee la "Matrice di visualizzazione-proiezione" ViewProjutilizzata per calcolare le coordinate dello schermo in cui verrà disegnata l'immagine filtrata:

// Uniform variables set by OBS (required)
uniform float4x4 ViewProj; // View-projection matrix used in the vertex shader
uniform Texture2D image;   // Texture containing the source picture

La definizione successiva è uno " stato del campionatore ". Definisce come campionare i colori dai pixel in una trama, cioè come interpolare i colori tra due pixel (Lineare) o semplicemente prendere il prossimo vicino (Punto), e come comportarsi quando le coordinate dei pixel sono al di fuori della trama, cioè prendi i pixel dal bordo più vicino (Morsetto), avvolgili (Avvolgi) o specchia la texture sui bordi (Specchia). I valori supportati per gli stati del campionatore sono documentati nel riferimento API OBS.

Definiamo un semplice stato del campionatore a morsetto lineare che verrà utilizzato nel pixel shader:

// Interpolation method and wrap mode for sampling a texture
SamplerState linear_clamp
{
    Filter    = Linear;     // Anisotropy / Point / Linear
    AddressU  = Clamp;      // Wrap / Clamp / Mirror / Border / MirrorOnce
    AddressV  = Clamp;      // Wrap / Clamp / Mirror / Border / MirrorOnce
    BorderColor = 00000000; // Used only with Border edges (optional)
};

Successivamente, vengono definite due strutture di dati molto specifiche per specificare quali parametri sono dati al vertex shader e passati dal vertex shader al pixel shader. In questo tutorial queste strutture sono separate artificialmente per una migliore comprensione e maggiore generalità. Nella maggior parte degli effetti, entrambe le strutture sono definite in una sola struttura.

Le strutture richiedono per ogni parametro un identificatore semantico che dia la destinazione d'uso del parametro. La semantica supportata è documentata nel riferimento API OBS. La semantica è necessaria per la pipeline grafica della GPU:

// Data type of the input of the vertex shader
struct vertex_data
{
    float4 pos : POSITION;  // Homogeneous space coordinates XYZW
    float2 uv  : TEXCOORD0; // UV coordinates in the source picture
};

// Data type of the output returned by the vertex shader, and used as input 
// for the pixel shader after interpolation for each pixel
struct pixel_data
{
    float4 pos : POSITION;  // Homogeneous screen coordinates XYZW
    float2 uv  : TEXCOORD0; // UV coordinates in the source picture
};

Prima di andare oltre, alcune parole sulla classica pipeline di rendering 3D. In parole povere, comprende questi passaggi principali:

  • L'applicazione, qui OBS, imposta tutte le strutture dati (trame, parametri dei dati, array, ecc.) Necessarie per il calcolo e quindi alimenta le coordinate 3D dei triangoli nel vertex shader insieme a vari attributi del vertice come la coordinata UV del vertice mappato a una texture o un colore assegnato a questo vertice, tutto parte della vertex_datastruttura.
  • Ogni volta che una vertex_datastruttura è disponibile nella GPU, il vertex shader viene chiamato e restituisce una pixel_datastruttura corrispondente al pixel sotto il vertice come si vede sullo schermo. Durante questa chiamata, il vertex shader trasforma le coordinate del mondo 3D del vertice in coordinate dello schermo e, se necessario, trasforma gli altri attributi del vertice.
  • Quindi molti passaggi complessi vengono eseguiti dalla GPU per determinare quanto è visibile la superficie di ciascun triangolo sullo schermo, fino a ciascun pixel visibile.
  • Ogni volta che la GPU determina che un pixel di un triangolo è visibile, chiama il pixel shader con una pixel_datastruttura come argomento che corrisponde alla posizione del pixel. Al fine di fornire valori specifici della posizione per ogni pixel del triangolo, la GPU interpola i valori dai valori delle 3 strutture restituite per i 3 vertici del triangolo dal vertex shader.

Per eseguire il rendering delle sorgenti, OBS segue lo stesso modello e rende semplicemente i quad (2 triangoli) sullo schermo, dove ogni vertice ha come attributo la mappatura UV dell'immagine sorgente. Il vertex shader viene utilizzato solo per calcolare le coordinate dello schermo dei 4 vertici del quad, in base alla trasformazione della sorgente, e per passare le coordinate UV al pixel shader. Il metodo classico per la trasformazione delle coordinate del mondo in coordinate dello schermo è una moltiplicazione tramite la matrice 4x4 "View-Projection" in coordinate omogenee (non temere, non è necessario comprendere coordinate omogenee per il tutorial):

// Vertex shader used to compute position of rendered pixels and pass UV
pixel_data vertex_shader_halftone(vertex_data vertex)
{
    pixel_data pixel;
    pixel.pos = mul(float4(vertex.pos.xyz, 1.0), ViewProj);
    pixel.uv  = vertex.uv;
    return pixel;
}

Il vertex shader rimarrà così nella maggior parte dei casi con OBS. Alterare la trasformazione 3D cambierebbe la posizione corretta delle sorgenti visualizzate sullo schermo. Si noti che si potrebbero ottenere risultati interessanti, ad esempio l'animazione Source Shake potrebbe essere certamente implementata come una modifica intelligente della matrice View-Projection.

Si prega di notare la sintassi HLSL ben adattata vertex.pos.xyzqui. posè un vertexmembro con tipo vettoriale float4. Aggiungendo il suffisso .xyzè sufficiente per convertirlo in un temporaneo float3vettore con i valori dei componenti x, ye zdi pos. Quindi float4(vertex.pos.xyz, 1.0)è di nuovo un float4vettore con i primi 3 componenti vertex.pos.xyze 1.0come quarto componente. La sintassi HLSL, che è molto simile a GLSL usata in OpenGL, ha questa caratteristica speciale che la rende molto compatta per le operazioni su matrici e vettori.

Ora il vero cuore dell'effetto del filtro video è nel Pixel Shader. Il principio principale di un pixel shader è molto semplice: questa è una funzione che calcola un colore in una data posizione del pixel. La funzione pixel shader viene chiamata per ogni singolo pixel all'interno dell'area di disegno.

Come un semplice effetto di ombreggiatura grigia, vogliamo calcolare la "luminanza" del pixel sorgente (cioè la sua luminosità o intensità luminosa), e usarla per ogni componente RGB del colore di output:

// Pixel shader used to compute an RGBA color at a given pixel position
float4 pixel_shader_halftone(pixel_data pixel) : TARGET
{
    float4 source_sample = image.Sample(linear_clamp, pixel.uv);
    float luminance = dot(source_sample.rgb, float3(0.299, 0.587, 0.114));
    return float4(luminance.xxx, source_sample.a);
}

Ammira ancora una volta la sintassi incredibilmente compatta:

  • Sulla prima riga, la source_samplevariabile riceve il colore RGBA del pixel presente pixel.uvnell'immagine sorgente in coordinate UV, seguendo un metodo di campionamento fornito da linear_clamp. Attenzione: qui .uvc'è solo un float2membro della struttura pixel, non un suffisso per accedere a particolari componenti del vettore.
  • Sulla seconda riga, la luminanza relativa viene calcolata come prodotto scalare del float3vettore delle componenti RGB sample_colore del float3vettore costante (0,299, 0,587, 0,114) . L'espressione è equivalente a source_sample.r*0.299 + source_sample.g*0.587 + source_sample.b*0.114. Suffissi r, g, b, apossono essere usati come x, y, z, wper accedere ai particolari componenti vettoriali. È necessario scrivere source_sample.rgbqui perché è un float4vettore e vogliamo escludere il acomponente dal prodotto scalare.
  • Sulla terza riga, float4viene creato un vettore con tre volte il luminancevalore e quindi il valore alfa originale del colore di origine come quarto componente. Il colore viene restituito come un float4contenente i valori dei componenti RGBA compresi tra 0,0 e 1,0.

La posizione del pixel è fornita tramite la pixel_datastruttura:

  • pixel.posè un float4vettore risultante dal calcolo nel vertex shader e da un'ulteriore interpolazione. Nel pixel shader pixel.pos.xypuò contenere la posizione assoluta sullo schermo del pixel da renderizzare (cioè i valori cambiano se l'utente sposta la sorgente con il mouse). Ora, se la sorgente è essa stessa ridimensionata con un'impostazione di Filtro in scala diversa da Disabilita , o se qualche altro filtro deve eseguire il rendering in una trama come passaggio intermedio, allora pixel.pos.xypotrebbe contenere alcune altre coordinate pixel corrispondenti alla trama interna utilizzata per eseguire il rendering dell'output del nostro filtro prima dell'ulteriore elaborazione. Inoltre, essendo pixel.posin coordinate omogenee, normalmente è necessario dividere xe yper ilwcomponenti (che dovrebbe essere sempre uguale a 1 qui perché non c'è 3D). Per farla breve, non è consigliabile utilizzare pixel.posdirettamente.
  • pixel.uvfornisce le coordinate UV interpolate del pixel nell'immagine sorgente come float2vettore, cioè non dipende dalla posizione o dal ridimensionamento della sorgente. Come coordinate UV, pixel.uv.xe pixel.uv.yhanno valori nell'intervallo da 0,0 a 1,0, con l'angolo superiore sinistro dell'immagine sorgente è nella posizione (0.0,0.0) e l'angolo inferiore destro in (1.0,1.0). Questo è ciò che vogliamo usare per fare riferimento ai pixel di origine. Inoltre, le coordinate UV possono essere utilizzate direttamente per recuperare il colore del pixel dalla imagetexture.

La parte finale del file degli effetti è la definizione delle "tecniche". Anche in questo caso la struttura sarà simile nella maggior parte degli effetti, tipicamente con un passaggio e una tecnica:

technique Draw
{
    pass
    {
        vertex_shader = vertex_shader_halftone(vertex);
        pixel_shader  = pixel_shader_halftone(pixel);
    }
}

Aggiungi tutti i blocchi di codice nel file HLSL, quindi riavvia OBS, normalmente l'origine dovrebbe essere visualizzata in bianco e nero:

filtrare la luminanza dei mezzitoni

Bene, questa non è proprio un'immagine a mezzitoni qui, ma mostra chiaramente le sfumature di grigio previste in base alla luminanza. L'immagine di esempio è molto scura e necessita di una correzione della luminosità.

Correzione gamma

I colori sono tipicamente codificati con 8 bit per ogni componente RGB, cioè nell'intervallo da 0 a 255, corrispondente all'intervallo da 0,0 a 1,0 in HLSL. Ma poiché l'occhio umano ha una percezione non lineare dell'intensità luminosa emessa dal monitor, solitamente i valori codificati nelle componenti RGB non crescono linearmente con l'intensità, seguono una legge di potenza con un esponente chiamato "gamma". Questa non linearità dei componenti RGB può essere un problema in un filtro video se vengono eseguiti dei calcoli assumendo che i componenti siano codificati in modo lineare.

In realtà, nel quadro di un filtro OBS, questo non è sempre un problema. Alcune fonti forniranno dati pixel linearizzati agli shader, altre forniranno dati codificati in Gama. A partire dalla versione 26.1, sono stati fatti diversi tentativi per avere colori linearizzati in modo coerente disponibili negli shader. Finché non è completamente implementato, per dare la massima flessibilità, lasceremo che l'utente scelga se la sorgente è codificata in gamma o meno. Poiché il filtro di questo tutorial è più artistico che esatto, un utente proverebbe semplicemente impostazioni diverse per ottenere il miglior risultato.

La legge di potenza per la codifica e la decodifica utilizza tipicamente un esponente 2.2 o 1 / 2.2 . Le due operazioni di correzione gamma sono chiamate prima compressione gamma , ovvero codifica di una componente RGB proporzionale all'intensità della luce in un valore da visualizzare all'occhio umano, con esponente inferiore a 1, ed espansione gamma con esponente maggiore di 1 .

Chiamiamo gamma l'esponente utilizzato per la decodifica del colore (maggiore di 1) con un valore di 2.2 . Possiamo usare formule semplificate per la codifica gamma:

_encoded_value = valore lineare 1 / gamma

E per la decodifica gamma:

_linear_value = gamma del valore codificato

Si noti che tali formule mantengono valori compresi tra 0,0 e 1,0.

Ora, per tornare al filtro dei mezzitoni, abbiamo notato che l'immagine di esempio è un po 'troppo scura e sappiamo che cambiando la "gamma" di un'immagine cambia la sua luminosità complessiva. Quindi possiamo introdurre un calcolo della gamma per 2 scopi: uso di valori lineari per calcoli più precisi e uno "spostamento gamma" dell'immagine sorgente (non chiamato "correzione gamma" per evitare malintesi).

Vale a dire, una decodifica gamma che includa uno spostamento sottrattivo sarebbe simile a questa, eseguita prima di qualsiasi calcolo sui componenti del colore RGB (con un segno meno tale che i valori positivi aumentano la luminosità):

_linear_value = spostamento gamma del valore codificato

Facciamolo nel codice, iniziando con la definizione delle variabili uniform gammae gamma_shifte dei valori di default con altre variabili uniform all'inizio del file dell'effetto HLSL:

// General properties
uniform float gamma = 1.0;
uniform float gamma_shift = 0.6;

Sperimenta e adatta i valori predefiniti di gammae gamma_shiftalla tua fonte. Verranno definiti in seguito tramite le proprietà OBS.

Introduciamo le funzioni di codifica e decodifica gamma e riscriviamo il pixel shader per utilizzarlo. Usiamo una clampfunzione aggiuntiva per essere sicuri di mantenere i valori tra 0,0 e 1,0 prima dell'elevamento a potenza. In effetti questo potrebbe non essere necessario ma evita un avviso che potrebbe essere prodotto dal compilatore. Aggiungi il codice sotto il vertex shader:

float3 decode_gamma(float3 color, float exponent, float shift)
{
    return pow(clamp(color, 0.0, 1.0), exponent - shift);
}

float3 encode_gamma(float3 color, float exponent)
{
    return pow(clamp(color, 0.0, 1.0), 1.0/exponent);
}

// Pixel shader used to compute an RGBA color at a given pixel position
float4 pixel_shader_halftone(pixel_data pixel) : TARGET
{
    float4 source_sample = image.Sample(linear_clamp, pixel.uv);
    float3 linear_color = decode_gamma(source_sample.rgb, gamma, gamma_shift);

    float luminance = dot(linear_color, float3(0.299, 0.587, 0.114));
    float3 result = luminance.xxx;

    return float4(encode_gamma(result, gamma), source_sample.a);
}

Aggiungi tutti i blocchi di codice nel file HLSL, quindi riavvia OBS (solo ricaricare lo script non sarebbe di aiuto, perché l'effetto rimane memorizzato nella cache e non verrebbe ricompilato). A seconda dei valori selezionati per gammae gamma_shift, l'immagine filtrata dovrebbe avere una luminosità complessiva diversa:

filtrare la luminanza con correzione gamma dei mezzitoni

L'immagine è un po 'migliore adesso.

Larghezza e altezza da Lua allo shader

Qualunque sia la forma finale, l'effetto dei mezzitoni si basa su un motivo applicato pixel per pixel all'immagine sorgente. Mentre è facile trasformare il colore di un singolo pixel, se la posizione del pixel è necessaria per riconoscere in quale parte di un motivo si trova il pixel, allora le coordinate UV da sole non sono sufficienti nel caso generale, è necessario per conoscere la dimensione dell'immagine di origine. Maggiori informazioni sui calcoli nella sezione successiva.

In questa sezione vedremo prima come rendere i valori di widthe heightdisponibili nello shader dal codice Lua. Stranamente, OBS non prevede questi parametri nei file effettivi di default. Definiamo le uniformvariabili con altre variabili uniformi all'inizio del file degli effetti HLSL:

// Size of the source picture
uniform int width;
uniform int height;

Solo le variabili uniformi possono essere modificate dal codice Lua. Una volta compilato un effetto, la funzione gs_effect_get_param_by_namefornisce la gs_eparamstruttura necessaria in cui è possibile impostare il valore.

I parametri dell'effetto possono essere recuperati alla fine del source_info.createnel file Lua (sopra return data):

  -- Retrieves the shader uniform variables
  data.params = {}
  data.params.width = obs.gs_effect_get_param_by_name(data.effect, "width")
  data.params.height = obs.gs_effect_get_param_by_name(data.effect, "height")

Infine i valori vengono impostati con gs_effect_set_intbetween obs_source_process_filter_begine obs_source_process_filter_endin in source_info.video_render:

  -- Effect parameters initialization goes here
  obs.gs_effect_set_int(data.params.width, data.width)
  obs.gs_effect_set_int(data.params.height, data.height)

Aggiungi il codice, riavvia OBS, per ora non è prevista alcuna differenza, le cose interessanti iniziano nella sezione successiva.

Perturbazione della luminanza

Ora che la dimensione dell'immagine è disponibile, possiamo iniziare a utilizzare le coordinate dei pixel per le formule. Vale a dire, vogliamo aggiungere una piccola perturbazione sulla luminanza calcolata secondo la formula cos (x) * cos (y) . La forma di questa formula classica si presenta come:

filtro mezzitoni

La formula sarà tale che:

  • Aggiunge alla luminanza un piccolo valore negativo o positivo con una data ampiezza
  • La scala del modulo può essere modificata, con una scala di 1.0 corrispondente ad un'oscillazione lunga 8 pixel

Se nominiamo i parametri della formula semplicemente scala e ampiezza , assumendo x ed y sono in pixel, l'angolo delle oscillazioni su x è dato da 2 * π * x / scala / 8 (maggiore è la scala , il più lungo le oscillazioni ). La funzione coseno restituisce valori compresi tra -1,0 e 1,0, quindi l' ampiezza può essere semplicemente moltiplicata per il prodotto del coseno.

La formula parametrizzata finale sarà (con semplificazione):

perturbazione = ampiezza * cos (π * x / scala / 4) * cos (π * y / scala / 4)

Per le coordinate x ed y in pixel, come abbiamo la larghezza e l'altezza più le coordinate UV, la formula sono semplicemente x = U * larghezza e y = V * altezza se chiamiamo coordinate UV U e V qui. Nel codice, useremo una forma più compatta con una moltiplicazione componente per componente di pixel.uvcon float2(width,height).

Riscrivi la parte centrale del pixel shader (tra le linee di decodifica e codifica) nel file dell'effetto HLSL:

    float luminance = dot(linear_color, float3(0.299, 0.587, 0.114));
    float2 position = pixel.uv * float2(width, height);
    float perturbation = amplitude * cos(PI*position.x/scale/4.0) * cos(PI*position.y/scale/4.0);
    float3 result = (luminance + perturbation).xxx;

Nella parte superiore del file, non dimenticare di definire una nuova costante per PIe le nuove variabili uniformi scalee amplitudecon valori predefiniti:

// Constants
#define PI 3.141592653589793238

// General properties
uniform float amplitude = 0.2;
uniform float scale = 1.0;

Aggiungi il codice, riavvia OBS, ora l'effetto dovrebbe essere simile a questo:

filtro perturbazione mezzitoni

Inizia lentamente a sembrare un mezzitoni.

Ridurre il numero di colori

Finora usiamo una luminanza continua. Il passaggio successivo consiste nell'imitare un numero ridotto di inchiostri su una carta stampata.

Per un dato valore di luminanza da 0,0 a 1,0, possiamo moltiplicare il valore per un fattore costante e quindi arrotondare il prodotto per ottenere un certo numero di valori interi. Ad esempio, con un fattore 3, otteniamo i valori 0, 1, 2 e 3. Dividiamo nuovamente per 3 per ottenere 4 valori di luminanza con valori 0.0, 0.33, 0.66 e 1.0. Può essere generalizzato a n colori moltiplicando / dividendo per n-1 .

Il calcolo è davvero semplice da implementare. Iniziamo aggiungendo un numero uniforme globale di livelli di colore con altre variabili uniformi nella parte superiore del file dell'effetto HLSL:

uniform int number_of_color_levels = 4.0;

E aggiungiamo una singola riga nel pixel shader, appena prima della codifica gamma:

    result = round((number_of_color_levels-1)*result)/(number_of_color_levels-1);

Aggiungi il codice, riavvia OBS, ora l'effetto dovrebbe essere simile a questo:

filtro mezzitoni 4 colori

Eccoci qui! È bello vedere cosa può fare una semplice perturbazione basata sul coseno più l'arrotondamento. La parte attiva dell'effetto è lunga solo un paio di righe.

E ora è anche molto interessante controllare come si comporta l'effetto con diversi tipi di filtro di scala (menu contestuale della sorgente in OBS, quindi Filtro di scala ).

Primo esempio, il punto di filtraggio della scala non esegue alcuna interpolazione dopo lo zoom e mostra pixel di forma quadrata:

filtro mezzitoni zoom scala punto di filtraggio

In genere, poiché utilizziamo un modello periodico nell'effetto, potrebbero apparire artefatti di "aliasing" se riduciamo le dimensioni dell'immagine:

filtro mezzitoni unzoom scala punto di filtraggio

Con un filtro di scala impostato su Bicubico , l'interpolazione dopo il ridimensionamento mostra l'anti-aliasing:

filtro mezzitoni zoom scala filtraggio bicubico

Con dimensioni ridotte, l'aliasing non è completamente sparito ma almeno ridotto:

filtro mezzitoni unzoom scala filtraggio bicubico

Ora un effetto molto interessante appare quando il filtro di scala è impostato su Disabilita su un'immagine fortemente ingrandita (attenzione potrebbe non funzionare se altri filtri sono nella catena della sorgente). Il pixel shader esegue il rendering direttamente sullo schermo (l'output non viene renderizzato in una texture intermedia per un successivo ridimensionamento), quindi viene chiamato per ogni singolo pixel nello spazio dello schermo, a livello di pixel secondario per l'immagine sorgente. Poiché utilizziamo funzioni matematiche continue e campioniamo l'immagine di origine utilizzando Linearun'interpolazione con linear_clamp, le curve tracciate dal pixel shader nascondono completamente la griglia di pixel dell'immagine di origine. Sembra un disegno vettoriale:

filtro mezzitoni zoom scala filtro disabilitato

Con dimensioni ridotte si comporta comunque bene:

filtro halftone unzoom scale filtering disable

L'effetto mezzitoni basato sul coseno è ora completamente implementato. Ha molti parametri impostati con valori predefiniti ma l'utente non può impostare questi parametri fino ad ora.

Aggiunta di proprietà

L'effetto è già soddisfacente, ora vogliamo migliorare l'esperienza degli utenti attraverso la creazione di proprietà per le variabili uniformi che già abbiamo nel file di effetto: gamma, gamma_shift, amplitude, scalee number_of_color_levels.

Per convenzione, denomineremo tutte le istanze delle variabili o delle proprietà con gli stessi nomi come nel file degli effetti, cioè per i parametri dell'effetto, le impostazioni dei dati e le variabili della datastruttura.

Analogamente a quanto descritto nel tutorial Source Shake, dobbiamo prima definire i valori predefiniti in source_info.get_defaults. I valori predefiniti sono scelti in modo tale che la loro applicazione a una nuova fonte darebbe un risultato ragionevole:

-- Sets the default settings for this source
source_info.get_defaults = function(settings)
  obs.obs_data_set_default_double(settings, "gamma", 1.0)
  obs.obs_data_set_default_double(settings, "gamma_shift", 0.0)
  obs.obs_data_set_default_double(settings, "scale", 1.0)
  obs.obs_data_set_default_double(settings, "amplitude", 0.2)
  obs.obs_data_set_default_int(settings, "number_of_color_levels", 4)
end

Quindi definiamo le proprietà come cursori in source_info.get_properties, che costruiscono e restituiscono una struttura delle proprietà:

-- Gets the property information of this source
source_info.get_properties = function(data)
  local props = obs.obs_properties_create()
  obs.obs_properties_add_float_slider(props, "gamma", "Gamma encoding exponent", 1.0, 2.2, 0.2)
  obs.obs_properties_add_float_slider(props, "gamma_shift", "Gamma shift", -2.0, 2.0, 0.01)
  obs.obs_properties_add_float_slider(props, "scale", "Pattern scale", 0.01, 10.0, 0.01)
  obs.obs_properties_add_float_slider(props, "amplitude", "Perturbation amplitude", 0.0, 2.0, 0.01)
  obs.obs_properties_add_int_slider(props, "number_of_color_levels", "Number of color levels", 2, 10, 1)

  return props
end

Una volta modificata una proprietà, source_info.updateviene chiamata la funzione. Qui è dove trasferiremo i valori dalle impostazioni dei dati alla datastruttura utilizzata per contenere i valori finché non vengono impostati nello shader:

-- Updates the internal data for this source upon settings change
source_info.update = function(data, settings)
  data.gamma = obs.obs_data_get_double(settings, "gamma")
  data.gamma_shift = obs.obs_data_get_double(settings, "gamma_shift")
  data.scale = obs.obs_data_get_double(settings, "scale")
  data.amplitude = obs.obs_data_get_double(settings, "amplitude")
  data.number_of_color_levels = obs.obs_data_get_int(settings, "number_of_color_levels")
end

Successivamente, come per widthe height, dobbiamo memorizzare i parametri dell'effetto e chiamiamo una volta source_info.updateper inizializzare i membri della datastruttura (non dimenticatelo, altrimenti OBS proverà a utilizzare dati non inizializzati source_info.video_rendere registrerà errori in ogni frame). Questo blocco si trova alla fine della source_info.createfunzione appena sopra return data:

  data.params.gamma = obs.gs_effect_get_param_by_name(data.effect, "gamma")
  data.params.gamma_shift = obs.gs_effect_get_param_by_name(data.effect, "gamma_shift")
  data.params.amplitude = obs.gs_effect_get_param_by_name(data.effect, "amplitude")
  data.params.scale = obs.gs_effect_get_param_by_name(data.effect, "scale")
  data.params.number_of_color_levels = obs.gs_effect_get_param_by_name(data.effect, "number_of_color_levels")

  -- Calls update to initialize the rest of the properties-managed settings
  source_info.update(data, settings)

E infine trasferiamo i valori dalla datastruttura ai parametri dell'effetto in source_info.video_render:

  obs.gs_effect_set_float(data.params.gamma, data.gamma)
  obs.gs_effect_set_float(data.params.gamma_shift, data.gamma_shift)
  obs.gs_effect_set_float(data.params.amplitude, data.amplitude)
  obs.gs_effect_set_float(data.params.scale, data.scale)
  obs.gs_effect_set_int(data.params.number_of_color_levels, data.number_of_color_levels)

Bene, è molto. Ogni variabile appare 7 volte in forme diverse! Ogni riga sopra è in qualche modo necessaria in modo che OBS gestisca la persistenza delle impostazioni, i valori predefiniti, la visualizzazione delle proprietà, ecc. Diciamo che ne vale la pena ma richiede una corretta progettazione orientata agli oggetti per evitare di scrivere tante volte le stesse righe di codice.

Aggiungi i pezzi di codice, riavvia OBS, apri i filtri della sorgente, dovrebbe assomigliare a questo (qui in 2 colori):

filtrare le proprietà dei mezzitoni

Giocare con i parametri di un filtro video e vedere immediatamente il risultato è abbastanza soddisfacente. Notare gli effetti di aliasing sull'anteprima nella finestra Filtri (non è chiaro se è possibile modificare il filtro lì).

L'effetto giornale vecchio stile è molto convincente con 2 colori:

filtro mezzitoni passero

Si noti che alcuni bordi sono conservati sull'immagine, quindi probabilmente non è lo stesso risultato che produrrebbe un processo ottico.

Le immagini in soli 3 colori possono essere affascinanti:

filtro mezzitoni lena

La prima parte del tutorial è completata e lo script e il file degli effetti sono sicuramente un buon punto di partenza per ulteriori sviluppi. È disponibile il codice sorgente completo di questa prima parte .

Seconda parte - Dithering con texture

In questa seconda parte vedremo come utilizzare texture aggiuntive per il pattern e la tavolozza dei colori.

Alcuni colori per favore

Se hai seguito il tutorial fino a questo punto, allora sei sicuramente stufo delle immagini in bianco e nero!

Modifichiamo solo una riga nel pixel shader:

    float3 result = linear_color + perturbation;

E questo è il risultato dopo aver riavviato OBS:

filtrare i colori dei mezzitoni

Funziona! Ed è un altro esempio della grande versatilità del linguaggio HLSL. Qui aggiungiamo lo float perturbationscalare a float3 linear_color(invece di float luminance). HLSL rende il casting necessario in modo trasparente (cioè converte perturbationin a float3).

Quindi aggiungiamo una piccola perturbazione ai canali Rosso, Verde e Blu. Quindi la quantizzazione del colore viene eseguita su ciascun canale rounddall'operazione, che si traduce in una tavolozza di 64 colori.

La formula del coseno scalare dà ottimi risultati e ha uno stile innegabile, ma mostra anche il limite del metodo. Piuttosto che un mix di punti con colori diversi, come apparirebbe una foto stampata ingrandita, osserviamo una griglia di tonalità di colore uniformi:

filtro mezzitoni lena colori

Quando il numero dei livelli di colore viene ridotto a 2, cioè con 8 colori (nero, bianco, rosso, verde, blu, ciano, magenta, giallo), è possibile ridurre anche il fattore di scala della perturbazione per ottenere un'immagine che riproduca colori originali (qui con un filtro di scala impostato su bicubico per mostrare i singoli pixel):

filtro mezzitoni lena colori ridotti

Non male con 8 colori e l'approccio primitivo! Ma il tipico "effetto carta stampata" non può essere raggiunto con l'attuale perturbazione scalare basata sul coseno, deve avere valori diversi su ciascun canale RGB. Per rendere la perturbazione completamente flessibile, sostituiremo la formula calcolata con una texture bitmap precalcolata.

Ri-fattorizzare il pixel shader

Il metodo generale per ottenere il nostro effetto può essere suddiviso in semplici passaggi:

  1. Il colore del pixel sorgente viene recuperato dalla imagetexture e decodificato in gamma
  2. Una perturbazione variabile viene determinata in base alla posizione del pixel sorgente
  3. La perturbazione viene aggiunta ai canali RGB del colore sorgente decodificato
  4. Un colore vicino al colore perturbato viene selezionato tra un insieme limitato tramite l'operazione di arrotondamento
  5. Il colore di chiusura è codificato in gamma e restituito come output

Innanzitutto, per essere molto generali, aggiungeremo una variabile offsetimpostata su 0 per impostazione predefinita al passaggio 3:

_perturbed_color = colore lineare + offset + ampiezza * perturbazione

Inoltre, permettiamo amplitudeche sia negativo. Quindi prima, nel file Lua, cambiamo la definizione della proprietà di ampiezza ( -2.0come limite inferiore):

  obs.obs_properties_add_float_slider(props, "amplitude", "Perturbation amplitude", -2.0, 2.0, 0.01)

E nel file degli effetti abbiamo bisogno di un valore uniforme aggiuntivo all'inizio del file:

uniform float offset = 0.0;

In secondo luogo, e questa è la modifica principale, i passaggi 2 e 4 vengono inseriti in funzioni separate per una migliore modularità. Definiremo anche il "colore di perturbazione" (determinato nel passaggio 2) e il colore più vicino (determinato come passaggio 4) con un alfa aggiuntivo, ovvero entrambi sono definiti come float4. La variabile alpha channel verrà trattata in seguito, per ora la stessa identica funzionalità verrà implementata con alpha impostato a 1.0.

Sostituisci il pixel shader con le 2 nuove funzioni e il nuovo codice per il pixel shader nel file dell'effetto:

float4 get_perturbation(float2 position)
{
    float4 result;
    result = float4((cos(PI*position.x/scale/4.0) * cos(PI*position.y/scale/4.0)).xxx, 1.0);
    return result;
}

float4 get_closest_color(float3 input_color)
{
    float4 result;
    result = float4(round((number_of_color_levels-1)*input_color)/(number_of_color_levels-1), 1.0);
    return result;
}

float4 pixel_shader_halftone(pixel_data pixel) : TARGET
{
    float4 source_sample = image.Sample(linear_clamp, pixel.uv);
    float3 linear_color = decode_gamma(source_sample.rgb, gamma, gamma_shift);

    float2 position = pixel.uv * float2(width, height);
    float4 perturbation = get_perturbation(position);

    float3 perturbed_color = linear_color + offset + amplitude*perturbation.rgb;

    float4 closest_color = get_closest_color(clamp(perturbed_color, 0.0, 1.0));

    return float4(encode_gamma(closest_color.rgb, gamma), source_sample.a);
}

Aggiungi il codice, riavvia OBS, non sono previste modifiche. Anche se amplitudeè impostato su un valore negativo, l'output è simile a causa della formula del coseno simmetrico.

Aggiunta delle proprietà della trama

Verranno aggiunte due texture:

  • Una trama senza cuciture per sostituire la formula del coseno con una perturbazione arbitraria basata su bitmap
  • Una texture tavolozza per recuperare i colori da un set limitato e quindi sostituire l'operazione di arrotondamento

La base per la gestione di una texture è un gs_image_fileoggetto. Viene selezionato come file dall'utente.

Abbiamo bisogno di alcune variabili aggiuntive per gestire la texture:

  • Ovviamente abbiamo bisogno degli texture2doggetti (ridefiniti come Texture2Dtipo da una macro per essere più conformi a HLSL)
  • Definiamo le dimensioni delle texture (non disponibile tramite Texture2Ddefinizione)
  • E definiamo un esponente di codifica / decodifica alfa per ogni evenienza

Per iniziare la definizione, aggiungi le seguenti variabili uniform nella parte superiore del file dell'effetto:

// Pattern texture
uniform Texture2D pattern_texture;
uniform float2 pattern_size = {-1.0, -1.0};
uniform float pattern_gamma = 1.0;

// Palette texture
uniform Texture2D palette_texture;
uniform float2 palette_size = {-1.0, -1.0};
uniform float palette_gamma = 1.0;

Venendo al file Lua, useremo l'opportunità per aggiungere il nuovo offsetusato nel calcolo delle perturbazioni (vedi sezione precedente). Aggiungi il recupero dei parametri dell'effetto alla source_info.createfunzione:

  data.params.offset = obs.gs_effect_get_param_by_name(data.effect, "offset")

  data.params.pattern_texture = obs.gs_effect_get_param_by_name(data.effect, "pattern_texture")
  data.params.pattern_size = obs.gs_effect_get_param_by_name(data.effect, "pattern_size")
  data.params.pattern_gamma = obs.gs_effect_get_param_by_name(data.effect, "pattern_gamma")

  data.params.palette_texture = obs.gs_effect_get_param_by_name(data.effect, "palette_texture")
  data.params.palette_size = obs.gs_effect_get_param_by_name(data.effect, "palette_size")
  data.params.palette_gamma = obs.gs_effect_get_param_by_name(data.effect, "palette_gamma")

L'aggiunta successivaèuna funzione di aiuto per impostare le dimensioni e i parametri dell'effetto trama secondo una logica semplice, cioè se l'oggetto tramaè nil, la sua dimensione è impostata su (-1, -1) in modo che lo shader sia in grado di riconoscerlo (e nota l'uso vec2dell'oggetto OBS):

function set_texture_effect_parameters(image, param_texture, param_size)
  local size = obs.vec2()
  if image then
    obs.gs_effect_set_texture(param_texture, image.texture)
    obs.vec2_set(size, image.cx, image.cy)
  else
    obs.vec2_set(size, -1, -1)
  end
  obs.gs_effect_set_vec2(param_size, size)
end

La funzione di supporto viene utilizzata nella source_info.video_renderfunzione, assumendo che gli oggetti del file immagine correlato che rappresentano il pattern e le trame della tavolozza siano memorizzati nella datavariabile passata da OBS:

  obs.gs_effect_set_float(data.params.offset, data.offset)

  -- Pattern texture
  set_texture_effect_parameters(data.pattern, data.params.pattern_texture, data.params.pattern_size)
  obs.gs_effect_set_float(data.params.pattern_gamma, data.pattern_gamma)

  -- Palette texture
  set_texture_effect_parameters(data.palette, data.params.palette_texture, data.params.palette_size)
  obs.gs_effect_set_float(data.params.palette_gamma, data.palette_gamma)

Continuiamo a implementare le trame con i valori predefiniti. Notare che le proprietà impostate dall'utente e mantenute nelle impostazioni dell'utente sono percorsi ai file di texture (vuoti se non è selezionata alcuna texture). Aggiungi le seguenti righe nella source_info.get_defaultsfunzione:

  obs.obs_data_set_default_double(settings, "offset", 0.0)

  obs.obs_data_set_default_string(settings, "pattern_path", "")
  obs.obs_data_set_default_double(settings, "pattern_gamma", 1.0)
  obs.obs_data_set_default_string(settings, "palette_path", "")
  obs.obs_data_set_default_double(settings, "palette_gamma", 1.0)

Per le proprietà, la funzione obs.obs_properties_add_pathviene utilizzata per consentire all'utente di selezionare un percorso. Siccome vogliamo essere in grado di fallback su una formula (senza texture), prevediamo un pulsante per ogni texture per reimpostare il percorso ad una stringa vuota (con una funzione inline che ritorna trueper forzare l'aggiornamento del widget delle proprietà e utilizza un mantenuto riferimento in data.settings, vedi sotto). Aggiungi le seguenti righe in source_info.get_properties:

  obs.obs_properties_add_float_slider(props, "offset", "Perturbation offset", -2.0, 2.0, 0.01)

  obs.obs_properties_add_path(props, "pattern_path", "Pattern path", obs.OBS_PATH_FILE,
                              "Picture (*.png *.bmp *.jpg *.gif)", nil)
  obs.obs_properties_add_float_slider(props, "pattern_gamma", "Pattern gamma exponent", 1.0, 2.2, 0.2)
  obs.obs_properties_add_button(props, "pattern_reset", "Reset pattern", function()
    obs.obs_data_set_string(data.settings, "pattern_path", ""); data.pattern = nil; return true; end)

  obs.obs_properties_add_path(props, "palette_path", "Palette path", obs.OBS_PATH_FILE,
                              "Picture (*.png *.bmp *.jpg *.gif)", nil)
  obs.obs_properties_add_float_slider(props, "palette_gamma", "Palette gamma exponent", 1.0, 2.2, 0.2)
  obs.obs_properties_add_button(props, "palette_reset", "Reset palette", function()
     obs.obs_data_set_string(data.settings, "palette_path", ""); data.palette = nil; return true; end)

Si aggiunge un'altra funzione di aiuto per gestire la lettura del file immagine, l'inizializzazione della texture interna, e lo svincolo della memoria allocata quando necessario. Nota che questo è possibile solo nel thread grafico (questo è il motivo della chiamata a obs_enter_graphics) e che la funzione gs_image_file_init_textureè separata dalla lettura dell'immagine:

-- Returns new texture and free current texture if loaded
function load_texture(path, current_texture)

  obs.obs_enter_graphics()

  -- Free any existing image
  if current_texture then
    obs.gs_image_file_free(current_texture)
  end

  -- Loads and inits image for texture
  local new_texture = nil
  if string.len(path) > 0 then
    new_texture = obs.gs_image_file()
    obs.gs_image_file_init(new_texture, path)
    if new_texture.loaded then
      obs.gs_image_file_init_texture(new_texture)
    else
      obs.blog(obs.LOG_ERROR, "Cannot load image " .. path)
      obs.gs_image_file_free(current_texture)
      new_texture = nil
    end
  end

  obs.obs_leave_graphics()
  return new_texture
end

Infine, la lettura dei file immagine viene attivata nella updatefunzione, ovvero quando le impostazioni dei dati sono state modificate dall'utente o all'avvio di OBS. Vengono definite variabili speciali per mantenere il percorso di un file immagine precedentemente caricato, in modo che possa essere liberato quando è necessario caricare un altro file. Notare che è necessario mantenere un riferimento data.settingsper le funzioni di callback inline dei pulsanti:

  -- Keeps a reference on the settings
  data.settings = settings

  data.offset = obs.obs_data_get_double(settings, "offset")

  local pattern_path = obs.obs_data_get_string(settings, "pattern_path")
  if data.loaded_pattern_path ~= pattern_path then
    data.pattern = load_texture(pattern_path, data.pattern)
    data.loaded_pattern_path = pattern_path
  end
  data.pattern_gamma = obs.obs_data_get_double(settings, "pattern_gamma")

  local palette_path = obs.obs_data_get_string(settings, "palette_path")
  if data.loaded_palette_path ~= palette_path then
    data.palette = load_texture(palette_path, data.palette)
    data.loaded_palette_path = palette_path
  end
  data.palette_gamma = obs.obs_data_get_double(settings, "palette_gamma")

Dopo aver aggiunto il codice, riavvia OBS e le proprietà dovrebbero apparire nella finestra di dialogo Filtri :

filtrare le trame delle proprietà dei mezzitoni

Le trame del modello e della tavolozza non sono ancora funzionali.

Retinatura basata su bitmap con pattern senza interruzioni

L'algoritmo ingenuo che usiamo è in realtà una forma di dithering ordinato , che tipicamente si basa su una "matrice di Bayer" sovrapposta all'immagine, dove ogni numero dalla matrice viene aggiunto al colore del relativo pixel e viene determinato il colore più vicino. In media, la combinazione di punti di colore vicini riproduce il colore originale. Nota che manteniamo questo semplice algoritmo, mentre esistono altri algoritmi per il dithering con una tavolozza arbitraria .

Nella sua forma matematica, una matrice Bayer contiene livelli tra 0 e 1 distribuiti sull'area della matrice. Le matrici Bayer possono essere rappresentate così come le immagini bitmap con scale di grigi, come in questo repository di texture PNG Bayer .

L'immagine della matrice Bayer 2x2 di base è un quadrato di 2 pixel per 2 pixel, ciò che rende un'immagine davvero minuscola:

Texture matrice Bayer con 4 livelli :: arrow_right filtro mezzitoni bayer 2x2:: arrow_left:

Dopo uno zoom:

filtro mezzitoni bayer 2x2 grande

Si noti che i 4 livelli di grigio 0, 1/4, 2/4, 3/4 aumentano lungo un ciclo come la lettera greca "alfa" (in alto a destra, in basso a sinistra, in alto a sinistra e in basso a destra). Questa costruzione garantisce che il motivo possa essere piastrellato all'infinito in ogni direzione senza artefatti visibili sui bordi.

Le matrici di Bayer di ordine superiore possono essere costruite in modo ricorsivo sostituendo ogni pixel con l'intera matrice e aggiungendo i livelli. Ad esempio, per una matrice 4x4 a partire dalla matrice 2x2, il pixel in alto a destra (livello 0 nero puro) viene sostituito direttamente con la matrice 2x2, quindi il pixel in basso a destra (livello 1/4) diventa la matrice 2x2 con 1 / 4 aggiunto ai livelli della matrice 2x2, ecc. Alla fine, la matrice Bayer 4x4 appare come (notare il blocco 2x2 in alto a destra, che è lo stesso della matrice Bayer 2x2):

Texture matrice Bayer con 16 livelli :: arrow_right filtro mezzitoni bayer 4x4:: arrow_left:

Dopo uno zoom:

filtro mezzitoni bayer 4x4 grande

Una variazione interessante con le matrici Bayer consiste nell'utilizzare una forma non rettangolare essa stessa disposta per creare un modello senza soluzione di continuità. Ad esempio con una croce di 5 pixel (quindi 5 livelli), disposta senza fori, risultante in una matrice minima 5x5:

Texture matrice Bayer con 5 livelli :: arrow_right filtro mezzitoni bayer cross 5 livelli:: arrow_left:

Dopo uno zoom:

filtro mezzitoni bayer cross 5 livelli grande

Ora abbiamo alcuni pattern con cui giocare, è il momento di modificare il file degli effetti.

Di solito, per trovare le coordinate in un pattern piastrellato, date le coordinate dell'intera area, la soluzione sarebbe quella di utilizzare un'operazione "modulo" (resto della divisione per la dimensione del pattern). Qui lasceremo che la GPU lo faccia per noi utilizzando una modalità di indirizzo texture "wrap" . Un nuovo sampler_state(ridefinito come SamplerStateda macro per essere più conforme a HLSL) è definito per quello nel file degli effetti:

SamplerState linear_wrap
{
    Filter    = Linear; 
    AddressU  = Wrap;
    AddressV  = Wrap;
};

Con questa definizione ridefiniamo la funzione get_perturbation:

float4 get_perturbation(float2 position)
{
    if (pattern_size.x>0)
    {
        float2 pattern_uv = position / pattern_size;
        float4 pattern_sample = pattern_texture.Sample(linear_wrap, pattern_uv / scale);
        float3 linear_color = decode_gamma(pattern_sample.rgb, pattern_gamma, 0.0);
        return float4(2.0*(linear_color-0.5), pattern_sample.a);
    }
    else
        return float4((cos(PI*position.x/scale/4.0) * cos(PI*position.y/scale/4.0)).xxx, 1.0);
}

Si prega di notare che:

  • Se la dimensione X del pattern è negativa, cioè se nessun file pattern è stato selezionato dall'utente o il file non può essere caricato, la funzione ricade sulla formula del coseno.
  • È necessario calcolare le coordinate UV pattern_uvper individuare il pixel nella trama del motivo, dato da position / pattern_size, ma poiché position(coordinate pixel nell'immagine sorgente) ha valori maggiori di pattern_size, le coordinate UV risultanti hanno valori compresi tra 0,0 e molto più di 1,0, e è qui che la modalità "Wrap" è importante.
  • La funzione dovrebbe restituire valori RGB da -1.0 a 1.0, ecco perché c'è una normalizzazione come 2.0*(linear_color-0.5)
  • L'utilizzo di un campionamento "lineare" (come in linear_wrap) è una scelta arbitraria qui e significa che se il motivo viene ridimensionato, i colori del motivo vengono interpolati (forse non l'aspettativa con una matrice Bayer). Potrebbe essere utilizzato anche un filtro "Punto", a seconda del tipo di ridimensionamento desiderato, in combinazione con l' impostazione Filtro scala .

Aggiungi il codice, riavvia OBS, quindi scarica e seleziona la matrice 4x4 Bayer sopra (quella minuscola), imposta la scala su 1.0, il filtro della scala su Bicubic e dovresti ottenere qualcosa del genere con 2 livelli di colore:

filtro mezzitoni lena bayer 4x4

Un'immagine del genere può riportare alla memoria i ricordi dell'era dei computer a 8 o 16 bit, a seconda della tua età, questo è quasi pixel art!

La texture basata sul pattern a croce con 5 livelli conferisce uno stile abbastanza diverso al dithering:

filtro mezzitoni lena bayer cross 5 livelli

Creazione di modelli senza soluzione di continuità

L'effetto è ora fortemente guidato dal pattern utilizzato come input. È necessario un modello "senza soluzione di continuità" e non è banale crearne uno. Ci sono strumenti speciali in giro per farlo, o c'è un metodo abbastanza semplice che funziona con qualsiasi strumento di disegno (ad esempio paint.net).

In realtà, il problema qui è che qualsiasi trasformazione globale applicata a un'immagine completa (ad es. Sfocatura gaussiana) presumerebbe che l'esterno dell'immagine abbia un colore costante (ad es. Bianco) e produrrebbe artefatti sui bordi che sono visibili solo quando l'immagine è piastrellato.

Il metodo semplice per evitarlo è "pre-affiancare" l'immagine su una griglia 9x9, quindi applicare la trasformazione globale, quindi selezionare e ritagliare la parte centrale dell'immagine. In questo modo gli artefatti del bordo sono limitati alle immagini circostanti e l'immagine centrale può essere piastrellata senza interruzioni:

filtro mezzitoni senza cuciture

L'uso di un filtro di sfocatura crea solo i diversi livelli previsti in uno schema di dithering. Questo è il motivo a stella risultante:

filtro mezzitoni pattern stella sfocatura

L'immagine filtrata mostra stelle con dimensioni diverse, che corrispondono ai diversi livelli creati dall'operazione di sfocatura (si otterrebbe un effetto migliore con più sfocature nel pattern, fare la propria sperimentazione):

filtro mezzitoni lena stelle

Lo stesso tipo di tecnica è stata utilizzata per sfocare una trama dalla pagina di piastrellatura esagonale su Wikipedia:

filtro mezzitoni esagoni sfocatura

In questo schema i canali RGB sono separati. La sfocatura è stata eseguita in un'unica operazione che ha mantenuto la separazione RGB. Il risultato ci riporta a produrre un'immagine a mezzitoni, questa volta con i colori. Guarda i tanti dischetti di diverse dimensioni, risultanti dalla sfocatura degli esagoni nella trama:

filtro mezzitoni lena occhi esagoni sfocatura

Infine, un altro metodo per produrre un bel pattern è semplicemente calcolarlo e prenderlo come uno screenshot. Ad esempio, questo shader sulla sandbox GLSL è stato utilizzato per produrre questo modello, che riproduce la forma della carta stampata, cioè con un raster ortogonale per ogni RGB e un orientamento di 0 ° per il rosso, 30 ° per il verde, 60 ° per il blu (lo schema si ripete dopo 30 oscillazioni, ecco perché l'immagine è così grande):

filtro pattern mezzetinte stampa raster

L'immagine risultante è molto simile a quella prodotta con gli esagoni sfocati:

filtro mezzitoni lena occhi stampa raster

La carta effettivamente stampata utilizza un modello di colore CMYK (ciano-magenta-giallo-nero) con una modalità di fusione sottrattiva e il nostro approccio semplice è classicamente additivo con i componenti RGB. Il calcolo reale con CMYK richiederebbe una modifica della formula perturbativa. Tuttavia questo semplice calcolo produce risultati convincenti diversi per riprodurre lo stile della carta stampata. Ciò è particolarmente vero per i fumetti o la pop art (dopo aver modificato l'ampiezza e l'offset):

filtro mezzitoni pattern pop art

Anche se il nostro filtro non produce il tipico raster di stampa CMYK, funziona molto bene in 8 colori perché mescolando i colori primari Rosso, Verde e Blu con una miscela additiva si ottengono direttamente i colori primari di una miscela sottrattiva Ciano (Verde + Blu), Magenta (rosso + blu) e giallo (rosso + verde). Alla fine, il set di colori risultante è lo stesso con l'RGB additivo o il CMYK sottrattivo, ed è per questo che l'impressione generale prodotta dall'output del filtro ricorda la carta stampata.

Tavolozza arbitraria

Fino ad ora la quantizzazione del colore si basa sulla ricerca del livello discreto più vicino su ogni componente RGB. L'operazione è semplice e veloce.

L'obiettivo ora è leggere i diversi colori da una trama e trovare quello più vicino. L'operazione implicherà, per ogni singolo pixel renderizzato, un confronto con un numero potenzialmente elevato di altri colori. Ovviamente l'operazione non sarà rapida e la tavolozza non potrà essere troppo grande.

Il sito web Lospec è una buona fonte per le tavolozze . Propone tavolozze curate con esempi di utilizzo in diversi formati. Per il nostro filtro, la cosa migliore è utilizzare il modulo di un pixel per colore, tutti i pixel in una singola riga.

Ad esempio, con questo formato, la tavolozza originale del Gameboy sarebbe simile a:

Tavolozza del Gameboy:: arrow_right filtro tavolozza mezzitoni gameboy:: arrow_left: Zoomed:filtro tavolozza mezzitoni gameboy grande

arrivando al codice, innanzitutto, per essere sicuri che i colori non vengano interpolati, è necessario un nuovo stato del campionatore utilizzando il filtro "Punto" nel file degli effetti:

SamplerState point_clamp
{
    Filter    = Point; 
    AddressU  = Clamp;
    AddressV  = Clamp;
};

Quindi, get_closest_colorviene modificato per includere una ricerca del colore. Il confronto di due colori si basa su una semplice distancefunzione. Questa è la stessa della solita distanza tra i punti 3D, applicata ai componenti RGB. Esistono molti metodi sofisticati di confronto dei colori , la distanza abituale sarà sufficiente per il nostro scopo qui.

La nuova funzione sostituisce get_closest_colornel file degli effetti:

float4 get_closest_color(float3 input_color)
{
    float4 result;
    if (palette_size.x>0)
    {
        float min_distance = 1e10;
        float2 pixel_size = 1.0 / min(256, palette_size);
        for (float u=pixel_size.x/2.0; u 0

  obs.obs_property_set_visible(obs.obs_properties_get(props, "pattern_reset"), pattern)
  obs.obs_property_set_visible(obs.obs_properties_get(props, "pattern_gamma"), pattern)

  obs.obs_property_set_visible(obs.obs_properties_get(props, "number_of_color_levels"), not palette)
  obs.obs_property_set_visible(obs.obs_properties_get(props, "palette_reset"), palette)
  obs.obs_property_set_visible(obs.obs_properties_get(props, "palette_gamma"), palette)

  return true
end

Quindi abbiamo ridefinito la source_info.get_propertiesfunzione come tale, con alcune ridenominazioni:

-- Gets the property information of this source
source_info.get_properties = function(data)
  print("In source_info.get_properties")

  local props = obs.obs_properties_create()

  local gprops = obs.obs_properties_create()
  obs.obs_properties_add_group(props, "input", "Input Source", obs.OBS_GROUP_NORMAL, gprops)
  obs.obs_properties_add_float_slider(gprops, "gamma", "Gamma encoding exponent", 1.0, 2.2, 0.2)
  obs.obs_properties_add_float_slider(gprops, "gamma_shift", "Gamma shift", -2.0, 2.0, 0.01)

  gprops = obs.obs_properties_create()
  obs.obs_properties_add_group(props, "pattern", "Dithering Pattern", obs.OBS_GROUP_NORMAL, gprops)
  obs.obs_properties_add_float_slider(gprops, "scale", "Pattern scale", 0.01, 10.0, 0.01)
  obs.obs_properties_add_float_slider(gprops, "amplitude", "Dithering amplitude", -2.0, 2.0, 0.01)
  obs.obs_properties_add_float_slider(gprops, "offset", "Dithering luminosity shift", -2.0, 2.0, 0.01)

  local p = obs.obs_properties_add_path(gprops, "pattern_path", "Pattern texture", obs.OBS_PATH_FILE,
                              "Picture (*.png *.bmp *.jpg *.gif)", nil)
  obs.obs_property_set_modified_callback(p, set_properties_visibility)
  obs.obs_properties_add_float_slider(gprops, "pattern_gamma", "Pattern gamma exponent", 1.0, 2.2, 0.2)
  obs.obs_properties_add_button(gprops, "pattern_reset", "Reset pattern texture", function(properties, property)
    obs.obs_data_set_string(data.settings, "pattern_path", ""); data.pattern = nil;
    set_properties_visibility(properties, property, data.settings); return true; end)

  gprops = obs.obs_properties_create()
  obs.obs_properties_add_group(props, "palette", "Color palette", obs.OBS_GROUP_NORMAL, gprops)
  obs.obs_properties_add_int_slider(gprops, "number_of_color_levels", "Number of color levels", 2, 10, 1)
  p = obs.obs_properties_add_path(gprops, "palette_path", "Palette texture", obs.OBS_PATH_FILE,
                              "Picture (*.png *.bmp *.jpg *.gif)", nil)
  obs.obs_property_set_modified_callback(p, set_properties_visibility)
  obs.obs_properties_add_float_slider(gprops, "palette_gamma", "Palette gamma exponent", 1.0, 2.2, 0.2)
  obs.obs_properties_add_button(gprops, "palette_reset", "Reset palette texture", function(properties, property)
    obs.obs_data_set_string(data.settings, "palette_path", ""); data.palette = nil;
    set_properties_visibility(properties, property, data.settings); return true; end)

  return props
end

La modifica con i gruppi è sempre la stessa: prima obs_propertiesviene creato un nuovo oggetto e aggiunto obs_propertiesall'oggetto principale , quindi il nuovo oggetto delle proprietà viene riempito di propertyoggetti.

Aggiungere il codice, ricaricare lo script, ora le proprietà dovrebbero essere visualizzate in gruppi denominati e alcuni parametri essere visualizzati solo quando necessario:

filtrare le proprietà finali dei mezzitoni

Consegna di un singolo file

Operazione finale su questo script, vogliamo fornire un singolo file Lua per facilitare la distribuzione. OBS dispone della gs_effect_createfunzione per compilare un effetto come una stringa e non come un file.

Per prima cosa dobbiamo copiare il contenuto completo del file dell'effetto in una grande stringa:

EFFECT = [[

// !! Copy the code of the effect file here !!

]]

Quindi cambia la compilazione in source_info.create(nota che il secondo parametro dovrebbe essere un nome di file e non può essere nil, il valore può essere usato per i messaggi di errore):

  data.effect = obs.gs_effect_create(EFFECT, "halftone_effect_code", nil)

E modifica la riga del messaggio di errore per rimuovere la menzione del percorso del file dell'effetto:

    obs.blog(obs.LOG_ERROR, "Effect compilation failed")

Nessun effetto visibile normalmente con questa modifica. È solo questione di mettere tutto in un file.

Conclusione

Woow è stato un lungo tutorial !! Anche il codice della seconda parte è disponibile.

Sono stati trattati molti aspetti, dalla scrittura di shader e script Lua alle basi dell'elaborazione video. La cosa più incredibile è la semplicità dell'algoritmo di base e gli effetti che produce.

C'è ancora spazio per ulteriori esperimenti: trasformazione CMYK reale, migliore confronto dei colori, effetti di trasparenza, design di modelli senza soluzione di continuità, ecc. Questo è lasciato come esercizio per il lettore!

È tutto gente!