diff --git a/src/textures/CMakeLists.txt b/src/textures/CMakeLists.txt
index eb8fe1a4..c21b8c4d 100644
--- a/src/textures/CMakeLists.txt
+++ b/src/textures/CMakeLists.txt
@@ -3,7 +3,7 @@ luisa_render_add_plugin(constant CATEGORY texture SOURCES constant.cpp)
luisa_render_add_plugin(image CATEGORY texture SOURCES image.cpp)
luisa_render_add_plugin(swizzle CATEGORY texture SOURCES swizzle.cpp)
luisa_render_add_plugin(scale CATEGORY texture SOURCES scale.cpp)
-luisa_render_add_plugin(concat CATEGORY texture SOURCES concat.cpp)
+luisa_render_add_plugin(multiply CATEGORY texture SOURCES multiply.cpp)
luisa_render_add_plugin(checkerboard CATEGORY texture SOURCES checkerboard.cpp)
# sky texture precomputation
diff --git a/src/textures/multiply.cpp b/src/textures/multiply.cpp
new file mode 100644
index 00000000..d3dff02e
--- /dev/null
+++ b/src/textures/multiply.cpp
@@ -0,0 +1,69 @@
+//
+// Created by mike on 4/17/24.
+//
+
+#include
+#include
+#include
+
+namespace luisa::render {
+
+class MultiplyTexture final : public Texture {
+
+private:
+ Texture *_a;
+ Texture *_b;
+
+public:
+ MultiplyTexture(Scene *scene, const SceneNodeDesc *desc) noexcept
+ : Texture{scene, desc},
+ _a{scene->load_texture(desc->property_node("a"))},
+ _b{scene->load_texture(desc->property_node("b"))} {}
+
+ [[nodiscard]] auto a() const noexcept { return _a; }
+ [[nodiscard]] auto b() const noexcept { return _b; }
+ [[nodiscard]] bool is_black() const noexcept override { return _a->is_black() || _b->is_black(); }
+ [[nodiscard]] bool is_constant() const noexcept override { return is_black() || (_a->is_constant() && _b->is_constant()); }
+ [[nodiscard]] luisa::optional evaluate_static() const noexcept override {
+ if (auto a = _a->evaluate_static(),
+ b = _b->evaluate_static();
+ a && b) {
+ return a.value() * b.value();
+ }
+ return nullopt;
+ }
+ [[nodiscard]] luisa::string_view impl_type() const noexcept override { return LUISA_RENDER_PLUGIN_NAME; }
+ [[nodiscard]] uint channels() const noexcept override { return std::min(_a->channels(), _b->channels()); }
+ [[nodiscard]] luisa::unique_ptr build(
+ Pipeline &pipeline, CommandBuffer &command_buffer) const noexcept override;
+};
+
+class MultiplyTextureInstance final : public Texture::Instance {
+
+private:
+ const Texture::Instance *_a;
+ const Texture::Instance *_b;
+
+public:
+ MultiplyTextureInstance(const Pipeline &pipeline,
+ const Texture *node,
+ const Texture::Instance *a,
+ const Texture::Instance *b) noexcept
+ : Texture::Instance{pipeline, node}, _a{a}, _b{b} {}
+ [[nodiscard]] Float4 evaluate(const Interaction &it,
+ const SampledWavelengths &swl,
+ Expr time) const noexcept override {
+ return _a->evaluate(it, swl, time) * _b->evaluate(it, swl, time);
+ }
+};
+
+luisa::unique_ptr MultiplyTexture::build(
+ Pipeline &pipeline, CommandBuffer &command_buffer) const noexcept {
+ auto a = pipeline.build_texture(command_buffer, _a);
+ auto b = pipeline.build_texture(command_buffer, _b);
+ return luisa::make_unique(pipeline, this, a, b);
+}
+
+}// namespace luisa::render
+
+LUISA_RENDER_MAKE_SCENE_NODE_PLUGIN(luisa::render::MultiplyTexture)