# Beispiel: Ein Filter-Plugin von null schreiben Dieses Dokument führt Schritt für Schritt durch die Erstellung eines neuen Filter-Plugins. Als Vorlage dient das eingebaute Vignette-Plugin (`plugins/builtin/filters/com.hms.fx.vignette/`), das ein GPU-Pass-Filter ist (PLAN.md §15.2). ## Ziel Wir erstellen ein Plugin `com.hms.fx.vignette` – einen Filter, der die Ränder eines Bildes abdunkelt oder einfärbt. Das Plugin unterstützt drei Backends: D3D11 (HLSL), OpenGL (GLSL) und OpenGL ES (GLES). ## Schritt 1: Verzeichnisstruktur anlegen Ein Plugin ist ein Verzeichnis mit `plugin.json` und Shader-Unterordnern je Backend: ```text com.hms.fx.vignette/ ├─ plugin.json └─ shaders/ ├─ d3d11/ │ └─ main.hlsl ├─ gl/ │ └─ main.frag └─ gles/ └─ main.frag ``` ## Schritt 2: Manifest schreiben Lege `plugin.json` an. Die Pflichtfelder sind `schema_version`, `id`, `name`, `version`, `api_version`, `kind`, `vendor`, `entrypoints`, `capabilities`, `failure_mode` (siehe [manifest-reference.md](manifest-reference.md)). ```json { "schema_version": 1, "id": "com.hms.fx.vignette", "name": "Vignette", "version": "1.0.0", "api_version": 1, "kind": "filter", "vendor": "HMS", "entrypoints": { "d3d11": { "type": "hlsl", "passes": [ {"pixel_shader": "shaders/d3d11/main.hlsl"} ] }, "gl": { "type": "glsl", "passes": [ {"fragment": "shaders/gl/main.frag"} ] }, "gles": { "type": "glsl_es", "passes": [ {"fragment": "shaders/gles/main.frag"} ] } }, "capabilities": { "minimum_tier": "PI_LITE", "requires_input_texture": true, "supported_backends": ["d3d11", "gl", "gles"] }, "parameters": [ {"id": "amount", "label": "Amount", "type": "float", "minimum": 0, "maximum": 1, "default": 0.5, "dmx_slots": [1]}, {"id": "radius", "label": "Radius", "type": "float", "minimum": 0, "maximum": 2, "default": 0.5, "dmx_slots": [2]}, {"id": "softness", "label": "Softness", "type": "float", "minimum": 0, "maximum": 1, "default": 0.4, "dmx_slots": [3]}, {"id": "roundness", "label": "Roundness", "type": "float", "minimum": 0, "maximum": 1, "default": 0.5, "dmx_slots": [4]}, {"id": "center_x", "label": "Center X", "type": "float", "minimum": 0, "maximum": 1, "default": 0.5, "dmx_slots": [5]}, {"id": "center_y", "label": "Center Y", "type": "float", "minimum": 0, "maximum": 1, "default": 0.5, "dmx_slots": [6]}, {"id": "color", "label": "Color", "type": "color", "default": [0, 0, 0], "dmx_slots": [7]}, {"id": "invert", "label": "Invert", "type": "bool", "default": 0, "dmx_slots": [8]}, {"id": "mix", "label": "Mix", "type": "float", "minimum": 0, "maximum": 1, "default": 1, "dmx_slots": []} ], "failure_mode": "bypass", "adaptive_quality": { "default": "auto", "variants": [ {"id": "low", "internal_scale": 1.0, "samples": 1}, {"id": "medium", "internal_scale": 1.0, "samples": 1}, {"id": "high", "internal_scale": 1.0, "samples": 1} ], "transition_ms": 180, "semantic_parameters_unchanged": ["mix"] } } ``` Wichtige Punkte: - `kind: "filter"` – verarbeitet eine Eingangs-Textur. - `requires_input_texture: true` – der Filter braucht eine Eingangs-Textur. - `dmx_slots` belegen die Slots 1–8; `mix` belegt keinen Slot (gemeinsamer Effektvertrag, §14.8). - `failure_mode: "bypass"` – bei Fehler wird der Effekt überbrückt (§3.5). - `adaptive_quality` deklariert drei Varianten; `semantic_parameters_unchanged: ["mix"]`. ## Schritt 3: HLSL-Shader schreiben (D3D11) Lege `shaders/d3d11/main.hlsl` an. Der Shader deklariert die Textur, den Sampler und den Konstantenpuffer `hms_params` mit dem Standard-Uniform-Satz (§14.4) und den Pluginparametern: ```hlsl // HMS MediaEngine - Vignette (PLAN.md §15.2) // Randabdunklung/-färbung; ein GPU-Pass. Texture2D u_input_texture : register(t0); SamplerState u_sampler : register(s0); cbuffer hms_params : register(b0) { float4 u_resolution; // xy = Auflösung in Pixeln float u_time_seconds; float u_delta_seconds; float u_frame_index; float u_layer_opacity; float u_audio_rms; float u_audio_peak; float u_audio_bass; float u_audio_mid; float u_audio_treble; float u_audio_beat; float param_amount; float param_radius; float param_softness; float param_roundness; float param_center_x; float param_center_y; float param_color_r; float param_color_g; float param_color_b; float param_invert; // 0 = abdunkeln, 1 = aufhellen float param_mix; float _pad0; float _pad1; float _pad2; float _pad3; }; float4 mainPS(float4 pos : SV_POSITION, float2 uv : TEXCOORD0) : SV_Target { float4 src = u_input_texture.Sample(u_sampler, uv); float m = saturate(param_mix); if (m < 0.01) { return src; // Mix 0 = kostenloser Bypass (§15.3) } float2 c = float2(param_center_x, param_center_y); float2 d = uv - c; d.x *= lerp(1.0, u_resolution.x / max(u_resolution.y, 1.0), param_roundness); float dist = length(d); float radius = max(param_radius, 0.0001); float soft = max(param_softness, 0.0001); float v = smoothstep(radius, radius + soft, dist); v = saturate(v * param_amount); if (param_invert > 0.5) v = 1.0 - v; float3 vignette = lerp(src.rgb, float3(param_color_r, param_color_g, param_color_b), v); float3 rgb = lerp(src.rgb, vignette, m); float4 out_c = float4(rgb, src.a); out_c.rgb *= u_layer_opacity; out_c.a *= u_layer_opacity; return out_c; } ``` ## Schritt 4: GLSL-Shader schreiben (OpenGL) Lege `shaders/gl/main.frag` an. Die Semantik ist identisch zur HLSL-Variante (§12.6, §15.4), nur die Syntax unterscheidet sich: ```glsl #version 330 core // HMS MediaEngine - Vignette (GLSL, Linux x64) // Semantik identisch zur HLSL-Variante (§12.6, §15.4). uniform sampler2D u_input_texture; uniform vec2 u_resolution; uniform float u_time_seconds; uniform float u_delta_seconds; uniform float u_frame_index; uniform float u_layer_opacity; uniform float u_audio_rms; uniform float u_audio_peak; uniform float u_audio_bass; uniform float u_audio_mid; uniform float u_audio_treble; uniform float u_audio_beat; uniform float param_amount; uniform float param_radius; uniform float param_softness; uniform float param_roundness; uniform float param_center_x; uniform float param_center_y; uniform float param_color_r; uniform float param_color_g; uniform float param_color_b; uniform float param_invert; uniform float param_mix; in vec2 v_uv; out vec4 fragColor; void main() { vec4 src = texture(u_input_texture, v_uv); float m = clamp(param_mix, 0.0, 1.0); if (m < 0.01) { fragColor = src; return; } vec2 c = vec2(param_center_x, param_center_y); vec2 d = v_uv - c; d.x *= mix(1.0, u_resolution.x / max(u_resolution.y, 1.0), param_roundness); float dist = length(d); float radius = max(param_radius, 0.0001); float soft = max(param_softness, 0.0001); float v = smoothstep(radius, radius + soft, dist); v = clamp(v * param_amount, 0.0, 1.0); if (param_invert > 0.5) v = 1.0 - v; vec3 vignette = mix(src.rgb, vec3(param_color_r, param_color_g, param_color_b), v); vec3 rgb = mix(src.rgb, vignette, m); vec4 out_c = vec4(rgb, src.a); out_c.rgb *= u_layer_opacity; out_c.a *= u_layer_opacity; fragColor = out_c; } ``` ## Schritt 5: GLES-Shader schreiben (OpenGL ES 2.0) Lege `shaders/gles/main.frag` an. GLES 2.0 hat eigene Regeln: `#version 100`, `precision mediump float;`, `texture2D` statt `texture`, `varying` statt `in`/`out`, `gl_FragColor` statt `fragColor` (siehe [shader-contract.md](shader-contract.md)): ```glsl #version 100 // HMS MediaEngine - Vignette (GLES, Raspberry Pi) // ES 2.0; ein GPU-Pass. precision mediump float; uniform sampler2D u_input_texture; uniform vec2 u_resolution; uniform float u_time_seconds; uniform float u_delta_seconds; uniform float u_frame_index; uniform float u_layer_opacity; uniform float u_audio_rms; uniform float u_audio_peak; uniform float u_audio_bass; uniform float u_audio_mid; uniform float u_audio_treble; uniform float u_audio_beat; uniform float param_amount; uniform float param_radius; uniform float param_softness; uniform float param_roundness; uniform float param_center_x; uniform float param_center_y; uniform float param_color_r; uniform float param_color_g; uniform float param_color_b; uniform float param_invert; uniform float param_mix; varying vec2 v_uv; void main() { vec4 src = texture2D(u_input_texture, v_uv); float m = clamp(param_mix, 0.0, 1.0); if (m < 0.01) { gl_FragColor = src; return; } vec2 c = vec2(param_center_x, param_center_y); vec2 d = v_uv - c; d.x *= mix(1.0, u_resolution.x / max(u_resolution.y, 1.0), param_roundness); float dist = length(d); float radius = max(param_radius, 0.0001); float soft = max(param_softness, 0.0001); float v = smoothstep(radius, radius + soft, dist); v = clamp(v * param_amount, 0.0, 1.0); if (param_invert > 0.5) v = 1.0 - v; vec3 vignette = mix(src.rgb, vec3(param_color_r, param_color_g, param_color_b), v); vec3 rgb = mix(src.rgb, vignette, m); vec4 out_c = vec4(rgb, src.a); out_c.rgb *= u_layer_opacity; out_c.a *= u_layer_opacity; gl_FragColor = out_c; } ``` ## Schritt 6: Validieren Das Plugin wird über den Lifecycle-Manager validiert (`packages/plugin_sdk/hms_plugin_sdk/lifecycle.py`). `discover()` lädt `plugin.json`, validiert es (inkl. Shader-Existenz) und überführt das Plugin in `validated` oder `quarantined`/`incompatible` (siehe [lifecycle.md](lifecycle.md)). Der Validator prüft unter anderem: - Manifest-Schema; - eindeutige Plugin-ID und semantische Version; - API-Kompatibilität; - Pfad- und ZIP-Sicherheit; - erlaubte Dateitypen und Größenlimits; - Shader-Kompilierung; - Capability-Prüfung; - vollständige, vorab kompilierbare Adaptive-Quality-Varianten und unveränderte Parametersemantik; - Lizenzmetadaten; - Hash des Pakets. ## Schritt 7: Aktivieren Nach erfolgreicher Validierung durchläuft das Plugin den Lebenszyklus `validated → installed → enabled → compiled → active`. Der Wechsel der Qualitätsstufe erfolgt atomar an einer Framegrenze (siehe [adaptive-quality.md](adaptive-quality.md)). ## Zusammenfassung Ein Filter-Plugin besteht aus: 1. `plugin.json` mit Pflichtfeldern, Parametern und Adaptive Quality; 2. je deklariertem Backend eine Shader-Datei mit dem Standard-Uniform-Satz (§14.4); 3. semantisch identischen Implementierungen über alle Backends (§12.6, §15.4).