Phase 3: Plugin-SDK-Dokumentation (docs/plugin-sdk/, 7 Dateien)
- README: Pluginarten, Verzeichnisstruktur, Quick Start (§14.1/§14.2) - manifest-reference: komplette plugin.json-Feldreferenz mit Validator-Regeln und ZIP-Sicherheit (§27.2) - shader-contract: Standard-Uniformsatz je Backend, cbuffer-Layout, 16-Byte-Alignment, Multipass, AQ-Bindung, GLES-Einschraenkungen (§14.4) - parameters-and-dmx: dmx_slots max 8 (§14.7), Kurven, 16-Bit-Werte, gemeinsamer Effektvertrag mix/blend/quality (§14.8) - lifecycle: Zustandsmaschine, Quarantaene, Show-Lock (§14.5/§26.3) - adaptive-quality: Varianten, Semantik-Erhalt, Kompilierung vor Aktivierung, Framegrenzen-Wechsel (§5.2) - example-walkthrough: komplettes Filter-Plugin anhand vignette - 1199 Zeilen, Deutsch, alle Beispiele aus echten Repo-Dateien
This commit is contained in:
@@ -0,0 +1,328 @@
|
||||
# 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).
|
||||
Reference in New Issue
Block a user