Start typing to search packages!
viewport-stencil
By @sebasvcx
Roblox
Mirrored from WallyViewportStencil
Render models inside surfaces in Roblox: ground cracks, holes, craters, portals, windows into walls. Nothing is cut or destroyed: a ViewportFrame on the surface shows the model as if you were looking through the surface into it, from any camera angle.
[image: A crack in a wall and another in the floor, both rendered with ViewportStencil]
How it works
ViewportStencil combines two existing techniques:
- rbx-viewport-window by EgoMoose: an off-axis projection that makes a ViewportFrame on a SurfaceGui line up with the world behind it, so it looks like a window instead of a flat image.
- ViewportFrame masking: faces whose vertex alpha has been erased render invisible inside a ViewportFrame, but still hide whatever is behind them. The model carries its own mask: a flat plane around the opening with erased alpha. Through the mask you see the real ground, and the rest of the model only shows through the opening.
The mask is part of the mesh, so each model defines its own shape. See Making a model.
Installation
Wally
[dependencies]
ViewportStencil = "sebasvcx/viewport-stencil@1.0.1"
Manually: download ViewportStencil.rbxm from the Releases page and put it in ReplicatedStorage.
Just want to try it? Download ViewportStencil-Demo.rbxl from the Releases page, open it in Studio and press Play.
See Example.
Example
The Releases page has a demo place, ViewportStencil-Demo.rbxl, ready to play: click anywhere
(floor or wall) to spawn a crack that disappears after 10 seconds.
What's in the demo:
ReplicatedStorage.ViewportStencil: the module.ReplicatedStorage.Assets.Crack.CrackTest: the crack model fromexample/crack.blend, set up as described in Making a model.ReplicatedStorage.Assets.Crack.VFX: particles and a purple light spawned with each crack. The light tints nearby stencils; see Lighting.StarterPlayerScripts.Example: the script that spawns the cracks,example/Example.client.lua.
In this repo, example/ has the demo script and the Blender file for the crack.
Usage
ViewportStencil only runs on the client.
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ViewportStencil = require(ReplicatedStorage.ViewportStencil)
local crack = ReplicatedStorage.Crack -- a Model
-- A CFrame on whatever the mouse is pointing at, randomly rotated around the surface normal
local cframe = ViewportStencil.Utils.fromMouse(nil, nil, math.random() * 2 * math.pi)
if cframe then
ViewportStencil.new(crack:Clone(), cframe, { lifetime = 10 })
end
The stencil takes ownership of the model (it's destroyed along with the stencil), so pass a clone.
Stencils work on any surface. The CFrame is a point on the surface, with its UpVector along the surface normal. The
Utils functions build it from a raycast, so walls and ceilings work the same as floors.
API
ViewportStencil.new(model: Model, cframe: CFrame, options: StencilOptions?): Stencil
Creates a stencil that renders model inside the surface at cframe.
| Option | Default | |
|---|---|---|
size: Vector2 | 90% of the model's footprint | Surface size in studs, along the CFrame's X and Z axes. The model is clipped outside it. |
lifetime: number | never | Seconds until the stencil destroys itself. |
destroyModel: boolean | true | When false, the model is unparented instead of destroyed. |
transparency: number | 0 | Transparency of the whole stencil. |
maxDistance: number | 1000 | Hidden beyond this distance from the camera. |
brightness: number | 1 | SurfaceGui.Brightness |
lightInfluence: number | 1 | SurfaceGui.LightInfluence |
ambient: Color3 | white | ViewportFrame.Ambient |
lightColor: Color3 | (140, 140, 140) | ViewportFrame.LightColor |
lightDirection: Vector3 | (-1, -1, -1) | ViewportFrame.LightDirection |
Stencil
stencil.model | The model being rendered. Don't reparent it. |
stencil:setCFrame(cframe) / getCFrame() | Moves the stencil. |
stencil:setSize(size: Vector2) / getSize() | Resizes the surface. |
stencil:setTransparency(t) / getTransparency() | Useful to fade it out before destroying it. |
stencil:destroy() | Also :Destroy(), so it works with Maid, Janitor, Trove... Safe to call more than once. |
stencil:isDestroyed() |
ViewportStencil
getActive(): { Stencil } | Every stencil that hasn't been destroyed. |
destroyAll() |
ViewportStencil.Utils
All of these return a CFrame ready for new, or nil if nothing was hit. angle (radians) spins the stencil around
the surface normal. params defaults to ignoring the local player's character.
fromMouse(params?, maxDistance?, angle?) | Whatever is under the mouse. |
fromScreenPoint(x, y, params?, maxDistance?, angle?) | Whatever is under a point in viewport coordinates. |
belowCharacter(character?, maxDistance?, angle?) | The ground under a character (the local player's by default). |
raycast(origin, direction, params?, angle?) | Whatever the ray hits. |
fromRaycastResult(result, angle?) | |
fromNormal(position, normal, angle?) |
Making a model
A model is a mesh with two parts: the mask, a flat plane on top with a hole in it, and the visible part, what
you see through the hole (the walls and bottom of a crack, for example). This walks through a ground crack in Blender;
the finished file is in example/crack.blend. The
DevForum post explains the masking and the erased-alpha
vertex paint in more detail.
In Blender
1. Model the mesh: a flat plane with the opening cut out, and the geometry that goes below it.
[image: Initial mesh]
2. Separate it into two parts: the mask (the plane) and the visible part (everything below). Vertex colors are stored per vertex, so if they shared vertices, erasing the mask's alpha would also fade the edges of the visible part.
[image: Separated mesh]
To see the erased alpha while painting, give the mesh a material with a Color Attribute node whose Alpha output goes into the Base Color of the Principled BSDF, and switch the viewport shading to Material Preview.
[image: Color Attribute node]
[image: Material Preview]
3. Switch to Vertex Paint mode.
[image: Vertex Paint mode]
4. Set the brush's blending mode to Erase Alpha.
[image: Erase Alpha]
5. Paint over the whole mask plane. Any part you miss will be visible in game.
[image: Painting the mask]
6. When you're done, the whole mask should look black with the material from step 2, and the visible part should be untouched.
[image: Finished mask]
7. Export both objects together as one FBX (no need to join them in Blender), then import the FBX into Studio as a single mesh, so both parts end up in one MeshPart.
In Studio
Put the imported MeshPart in a Model and set it as the model's PrimaryPart:
Crack (Model, PrimaryPart = Crack)
└── Crack (MeshPart)
- The top of the
PrimaryPartis placed flush with the surface, so the mask plane must be the highest point of the mesh. Without aPrimaryPart, the top of the model's bounding box is used. - The model's up is its
PrimaryPart's UpVector (world up without aPrimaryPart), aligned with the surface normal. - Keep the opening centered on the mesh: the surface is centered on the stencil's CFrame.
- By default the surface is 90% of the model's footprint. Leaving the mask's outer edges out of the surface hides a
thin line of light that otherwise shows along them. If you pass your own
size, keep it a bit smaller than the mask. - Keep the mask tight around the opening. The viewport's pixels are spread over the whole surface, so a lot of empty mask around a small crack makes it blurrier.
- Anything else in the model (extra parts, effects) works as long as it stays below the mask.
Lighting
A stencil is lit in two separate layers, and both change how the model looks:
- The ViewportFrame's own lighting. Objects inside a ViewportFrame don't use
Lightingor any lights in the world. They only get the ViewportFrame'sAmbientlight and one directional light (LightColorandLightDirection). This is where the model gets its shading. - World lighting on the SurfaceGui. Once the ViewportFrame's image is drawn on the surface, the SurfaceGui is lit
by the world like any other surface, scaled by
LightInfluence. WithlightInfluence = 1, a colored light near the stencil tints it. With0, it ignores world lighting and shows the ViewportFrame's image as is.
brightness (SurfaceGui.Brightness) multiplies the final result. It's useful for glowing effects.
The defaults (white ambient, lightInfluence = 1) light the model evenly and let it pick up the world's lights. That
suits glowing, magical cracks, but a plain grey mesh will look flat and bright, and will take the color of any nearby
light.
Some starting points:
-- Realistic hole: dark inside, lit from above, ignores world lights
ViewportStencil.new(model, cframe, {
ambient = Color3.fromRGB(60, 60, 60),
lightColor = Color3.fromRGB(200, 200, 200),
lightDirection = Vector3.new(0, -1, 0),
lightInfluence = 0,
})
-- Glowing crack: fully lit and brighter than its surroundings
ViewportStencil.new(model, cframe, {
ambient = Color3.new(1, 1, 1),
lightInfluence = 0,
brightness = 2,
})
-- Blends with the scene: shaded by the viewport, tinted by nearby lights
ViewportStencil.new(model, cframe, {
ambient = Color3.fromRGB(120, 120, 120),
lightInfluence = 1,
})
lightDirection is the direction the light travels, in world space: (0, -1, 0) shines straight down and lights
upward-facing surfaces. The default (-1, -1, -1) comes diagonally from above.
The model's own colors, materials and textures still apply inside the ViewportFrame, with some limits: ViewportFrames don't render shadows or post-processing, and Neon and Glass render at the lowest quality, so Neon shows as a flat, bright color that doesn't glow or light up anything around it.
Performance
- Only stencils that are on screen, within
maxDistanceand in front of their surface are rendered. The rest have their SurfaceGui disabled and skip their per-frame update. - When the camera doesn't move, nothing is updated.
- The real cost is the GPU rendering each visible ViewportFrame, so keep the number of stencils on screen reasonable and the meshes simple.
Limitations
- Client only.
- ViewportFrames have no anti-aliasing, so edges are slightly jagged.
- The surface is a rectangle; the mask on the model is what gives it its shape.
- Overlapping stencils don't merge: the one on top covers the other.
Development
rojo serve dev.project.json
dev.project.json syncs the library into ReplicatedStorage.ViewportStencil and the example from example/ into
StarterPlayerScripts. default.project.json is just the library, used for Wally and rojo build.
Credits
- EgoMoose for rbx-viewport-window.
- ViewportFrame masking on the DevForum.
License
MIT
Package Details
Install command (Click to copy)
Version
1.0.1
License
MIT
Safe for commercial use
Automated license review — not legal advice.
